diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e94d0757..f3539fe13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -286,24 +286,6 @@ jobs: run: | target/debug/xtask ci wasm-pack ${{ matrix.cmd }} - formatting: - name: Check Formatting - runs-on: ubuntu-latest - - steps: - - name: Checkout the repo - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2024-11-26 - components: rustfmt - - - name: Cargo fmt - run: | - cargo fmt -- --check - typos: name: Spell Check with Typos runs-on: ubuntu-latest @@ -315,8 +297,8 @@ jobs: - name: Check the spelling of the files in our repo uses: crate-ci/typos@v1.29.4 - clippy: - name: Run clippy + lint: + name: Lint needs: xtask runs-on: ubuntu-latest @@ -333,7 +315,7 @@ jobs: uses: dtolnay/rust-toolchain@master with: toolchain: nightly-2024-11-26 - components: clippy + components: clippy, rustfmt - name: Load cache uses: Swatinem/rust-cache@v2 @@ -347,6 +329,10 @@ jobs: key: "${{ needs.xtask.outputs.cachekey-linux }}" fail-on-cache-miss: true + - name: Check Formatting + run: | + target/debug/xtask ci style + - name: Clippy run: | target/debug/xtask ci clippy diff --git a/Cargo.lock b/Cargo.lock index 7a966b599..1cc14a8d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,6 +1410,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "emojis" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" +dependencies = [ + "phf", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1684,9 +1693,9 @@ dependencies = [ [[package]] name = "eyeball-im" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1c02432230060cae0621e15803e073976d22974e0f013c9cb28a4ea1b484629" +checksum = "ad276eb017655257443d34f27455f60e8b02b839c6ebcaa8d6f06cc498784e8f" dependencies = [ "futures-core", "imbl", @@ -1696,9 +1705,9 @@ dependencies = [ [[package]] name = "eyeball-im-util" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a70e454238b5f66a0a0544c3e6a38be765cb01f34da9b94a2f3ecd8777cf8" +checksum = "eac7f06ce388e4f64876ad3836b275d0972ab64ae8bd8456862d5ebdb7bec4f5" dependencies = [ "arrayvec", "eyeball-im", @@ -2476,9 +2485,9 @@ dependencies = [ [[package]] name = "imbl" -version = "3.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc3be8d8cd36f33a46b1849f31f837c44d9fa87223baee3b4bd96b8f11df81eb" +checksum = "5ae128b3bc67ed43ec0a7bb1c337a9f026717628b3c4033f07ded1da3e854951" dependencies = [ "bitmaps", "imbl-sized-chunks", @@ -2490,9 +2499,9 @@ dependencies = [ [[package]] name = "imbl-sized-chunks" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144006fb58ed787dcae3f54575ff4349755b00ccc99f4b4873860b654be1ed63" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" dependencies = [ "bitmaps", ] @@ -3505,6 +3514,7 @@ dependencies = [ "async-stream", "async_cell", "chrono", + "emojis", "eyeball", "eyeball-im", "eyeball-im-util", @@ -3531,7 +3541,9 @@ dependencies = [ "tokio-stream", "tracing", "unicode-normalization", + "unicode-segmentation", "uniffi", + "url", "wiremock", ] @@ -4376,7 +4388,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.13.0", "proc-macro2", "quote", "syn", @@ -6149,9 +6161,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" diff --git a/Cargo.toml b/Cargo.toml index d989ceae5..bd5cdb4ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,8 @@ base64 = "0.22.1" byteorder = "1.5.0" chrono = "0.4.38" eyeball = { version = "0.8.8", features = ["tracing"] } -eyeball-im = { version = "0.5.1", features = ["tracing"] } -eyeball-im-util = "0.7.0" +eyeball-im = { version = "0.6.0", features = ["tracing"] } +eyeball-im-util = "0.8.0" futures-core = "0.3.31" futures-executor = "0.3.21" futures-util = "0.3.31" @@ -44,7 +44,7 @@ growable-bloom-filter = "2.1.1" hkdf = "0.12.4" hmac = "0.12.1" http = "1.1.0" -imbl = "3.0.0" +imbl = "4.0.1" indexmap = "2.6.0" insta = { version = "1.41.1", features = ["json"] } itertools = "0.13.0" diff --git a/README.md b/README.md index 825cd8036..bbf393296 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,9 @@ The rust-sdk consists of multiple crates that can be picked at your convenience: ## Status -The library is in an alpha state, things that are implemented generally work but -the API will change in breaking ways. +The library is considered production ready and backs multiple client implementations such as Element X [[1]](https://github.com/element-hq/element-x-ios) [[2]](https://github.com/element-hq/element-x-android) and [Fractal](https://gitlab.gnome.org/World/fractal). Client developers should feel confident to build upon it. -If you are interested in using the matrix-sdk now is the time to try it out and -provide feedback. +Development of the SDK has been primarily sponsored by Element though accepts contributions from all. ## Bindings diff --git a/benchmarks/benches/store_bench.rs b/benchmarks/benches/store_bench.rs index bc6cd8f28..9ec0b115f 100644 --- a/benchmarks/benches/store_bench.rs +++ b/benchmarks/benches/store_bench.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::StoreConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, Client, RoomInfo, RoomState, StateChanges, }; use matrix_sdk_base::{store::MemoryStore, SessionMeta, StateStore as _}; diff --git a/bindings/matrix-sdk-crypto-ffi/src/lib.rs b/bindings/matrix-sdk-crypto-ffi/src/lib.rs index e6c09013e..50e99cb50 100644 --- a/bindings/matrix-sdk-crypto-ffi/src/lib.rs +++ b/bindings/matrix-sdk-crypto-ffi/src/lib.rs @@ -680,15 +680,20 @@ pub struct EncryptionSettings { impl From for RustEncryptionSettings { fn from(v: EncryptionSettings) -> Self { + let sharing_strategy = if v.only_allow_trusted_devices { + CollectStrategy::OnlyTrustedDevices + } else if v.error_on_verified_user_problem { + CollectStrategy::ErrorOnVerifiedUserProblem + } else { + CollectStrategy::AllDevices + }; + RustEncryptionSettings { algorithm: v.algorithm.into(), rotation_period: Duration::from_secs(v.rotation_period), rotation_period_msgs: v.rotation_period_msgs, history_visibility: v.history_visibility.into(), - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: v.only_allow_trusted_devices, - error_on_verified_user_problem: v.error_on_verified_user_problem, - }, + sharing_strategy, } } } diff --git a/bindings/matrix-sdk-ffi/src/authentication.rs b/bindings/matrix-sdk-ffi/src/authentication.rs index a2e41a5f9..3cbf57654 100644 --- a/bindings/matrix-sdk-ffi/src/authentication.rs +++ b/bindings/matrix-sdk-ffi/src/authentication.rs @@ -5,7 +5,7 @@ use std::{ }; use matrix_sdk::{ - oidc::{ + authentication::oidc::{ registrations::OidcRegistrationsError, types::{ iana::oauth::OAuthClientAuthenticationMethod, diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index af8ddb438..d2431c25e 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -7,11 +7,7 @@ use std::{ use anyhow::{anyhow, Context as _}; use matrix_sdk::{ - media::{ - MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters, - MediaThumbnailSettings, - }, - oidc::{ + authentication::oidc::{ registrations::{ClientId, OidcRegistrations}, requests::account_management::AccountManagementActionFull, types::{ @@ -23,6 +19,10 @@ use matrix_sdk::{ }, OidcAuthorizationData, OidcSession, }, + media::{ + MediaFileHandle as SdkMediaFileHandle, MediaFormat, MediaRequestParameters, + MediaThumbnailSettings, + }, reqwest::StatusCode, ruma::{ api::client::{ @@ -1535,10 +1535,13 @@ impl Session { match auth_api { // Build the session from the regular Matrix Auth Session. AuthApi::Matrix(a) => { - let matrix_sdk::matrix_auth::MatrixSession { + let matrix_sdk::authentication::matrix::MatrixSession { meta: matrix_sdk::SessionMeta { user_id, device_id }, tokens: - matrix_sdk::matrix_auth::MatrixSessionTokens { access_token, refresh_token }, + matrix_sdk::authentication::matrix::MatrixSessionTokens { + access_token, + refresh_token, + }, } = a.session().context("Missing session")?; Ok(Session { @@ -1553,10 +1556,10 @@ impl Session { } // Build the session from the OIDC UserSession. AuthApi::Oidc(api) => { - let matrix_sdk::oidc::UserSession { + let matrix_sdk::authentication::oidc::UserSession { meta: matrix_sdk::SessionMeta { user_id, device_id }, tokens: - matrix_sdk::oidc::OidcSessionTokens { + matrix_sdk::authentication::oidc::OidcSessionTokens { access_token, refresh_token, latest_id_token, @@ -1617,12 +1620,12 @@ impl TryFrom for AuthSession { .transpose() .context("OIDC latest_id_token is invalid.")?; - let user_session = matrix_sdk::oidc::UserSession { + let user_session = matrix_sdk::authentication::oidc::UserSession { meta: matrix_sdk::SessionMeta { user_id: user_id.try_into()?, device_id: device_id.into(), }, - tokens: matrix_sdk::oidc::OidcSessionTokens { + tokens: matrix_sdk::authentication::oidc::OidcSessionTokens { access_token, refresh_token, latest_id_token, @@ -1639,12 +1642,12 @@ impl TryFrom for AuthSession { Ok(AuthSession::Oidc(session.into())) } else { // Create a regular Matrix Session. - let session = matrix_sdk::matrix_auth::MatrixSession { + let session = matrix_sdk::authentication::matrix::MatrixSession { meta: matrix_sdk::SessionMeta { user_id: user_id.try_into()?, device_id: device_id.into(), }, - tokens: matrix_sdk::matrix_auth::MatrixSessionTokens { + tokens: matrix_sdk::authentication::matrix::MatrixSessionTokens { access_token, refresh_token, }, diff --git a/bindings/matrix-sdk-ffi/src/error.rs b/bindings/matrix-sdk-ffi/src/error.rs index 5ce1bf48e..7e42d246f 100644 --- a/bindings/matrix-sdk-ffi/src/error.rs +++ b/bindings/matrix-sdk-ffi/src/error.rs @@ -1,15 +1,15 @@ use std::{collections::HashMap, fmt, fmt::Display}; use matrix_sdk::{ - encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, reqwest, - room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError, + authentication::oidc::OidcError, encryption::CryptoStoreError, event_cache::EventCacheError, + reqwest, room::edit::EditError, send_queue::RoomSendQueueError, HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError, QueueWedgeError as SdkQueueWedgeError, StoreError, }; use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline}; use uniffi::UnexpectedUniFFICallbackError; -use crate::room_list::RoomListError; +use crate::{room_list::RoomListError, timeline::FocusEventError}; #[derive(Debug, thiserror::Error)] pub enum ClientError { @@ -161,6 +161,12 @@ impl From for ClientError { } } +impl From for ClientError { + fn from(e: FocusEventError) -> Self { + Self::new(e) + } +} + /// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple /// String. /// diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index f4a937967..deaf91a9d 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -14,6 +14,7 @@ mod error; mod event; mod helpers; mod identity_status_change; +mod live_location_share; mod notification; mod notification_settings; mod platform; diff --git a/bindings/matrix-sdk-ffi/src/live_location_share.rs b/bindings/matrix-sdk-ffi/src/live_location_share.rs new file mode 100644 index 000000000..940fb48ca --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/live_location_share.rs @@ -0,0 +1,32 @@ +// Copyright 2024 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 crate::ruma::LocationContent; +#[derive(uniffi::Record)] +pub struct LastLocation { + /// The most recent location content of the user. + pub location: LocationContent, + /// A timestamp in milliseconds since Unix Epoch on that day in local + /// time. + pub ts: u64, +} +/// Details of a users live location share. +#[derive(uniffi::Record)] +pub struct LiveLocationShare { + /// The user's last known location. + pub last_location: LastLocation, + /// The live status of the live location share. + pub(crate) is_live: bool, + /// The user ID of the person sharing their live location. + pub user_id: String, +} diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 62f7b6780..818ac3ff3 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -4,14 +4,13 @@ use anyhow::{Context, Result}; use futures_util::{pin_mut, StreamExt}; use matrix_sdk::{ crypto::LocalTrust, - event_cache::paginator::PaginatorError, room::{ edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole, }, ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, RoomHero as SdkRoomHero, RoomMemberships, RoomState, }; -use matrix_sdk_ui::timeline::{default_event_filter, PaginationError, RoomExt, TimelineFocus}; +use matrix_sdk_ui::timeline::{default_event_filter, RoomExt}; use mime::Mime; use ruma::{ api::client::room::report_content, @@ -29,19 +28,23 @@ use ruma::{ EventId, Int, OwnedDeviceId, OwnedUserId, RoomAliasId, UserId, }; use tokio::sync::RwLock; -use tracing::error; +use tracing::{error, warn}; use super::RUNTIME; use crate::{ chunk_iterator::ChunkIterator, client::{JoinRule, RoomVisibility}, error::{ClientError, MediaInfoError, NotYetImplemented, RoomError}, - event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType}, + event::{MessageLikeEventType, StateEventType}, identity_status_change::IdentityStatusChange, + live_location_share::{LastLocation, LiveLocationShare}, room_info::RoomInfo, room_member::RoomMember, - ruma::{ImageInfo, Mentions, NotifyType}, - timeline::{DateDividerMode, FocusEventError, ReceiptType, SendHandle, Timeline}, + ruma::{ImageInfo, LocationContent, Mentions, NotifyType}, + timeline::{ + configuration::{AllowedMessageTypes, TimelineConfiguration}, + ReceiptType, SendHandle, Timeline, + }, utils::u64_to_uint, TaskHandle, }; @@ -87,10 +90,6 @@ impl Room { #[matrix_sdk_ffi_macros::export] impl Room { - pub fn id(&self) -> String { - self.inner.room_id().to_string() - } - /// Returns the room's name from the state event if available, otherwise /// compute a room name based on the room's nature (DM or not) and number of /// members. @@ -200,115 +199,44 @@ impl Room { } } - /// Returns a timeline focused on the given event. - /// - /// Note: this timeline is independent from that returned with - /// [`Self::timeline`], and as such it is not cached. - pub async fn timeline_focused_on_event( + /// Build a new timeline instance with the given configuration. + pub async fn timeline_with_configuration( &self, - event_id: String, - num_context_events: u16, - internal_id_prefix: Option, - ) -> Result, FocusEventError> { - let parsed_event_id = EventId::parse(&event_id).map_err(|err| { - FocusEventError::InvalidEventId { event_id: event_id.clone(), err: err.to_string() } - })?; - - let room = &self.inner; - - let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room); - - if let Some(internal_id_prefix) = internal_id_prefix { - builder = builder.with_internal_id_prefix(internal_id_prefix); - } - - let timeline = match builder - .with_focus(TimelineFocus::Event { target: parsed_event_id, num_context_events }) - .build() - .await - { - Ok(t) => t, - Err(err) => { - if let matrix_sdk_ui::timeline::Error::PaginationError( - PaginationError::Paginator(PaginatorError::EventNotFound(..)), - ) = err - { - return Err(FocusEventError::EventNotFound { event_id: event_id.to_string() }); - } - return Err(FocusEventError::Other { msg: err.to_string() }); - } - }; - - Ok(Timeline::new(timeline)) - } - - pub async fn pinned_events_timeline( - &self, - internal_id_prefix: Option, - max_events_to_load: u16, - max_concurrent_requests: u16, - ) -> Result, ClientError> { - let room = &self.inner; - - let mut builder = matrix_sdk_ui::timeline::Timeline::builder(room); - - if let Some(internal_id_prefix) = internal_id_prefix { - builder = builder.with_internal_id_prefix(internal_id_prefix); - } - - let timeline = builder - .with_focus(TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests }) - .build() - .await?; - - Ok(Timeline::new(timeline)) - } - - /// A timeline instance that can be configured to only include RoomMessage - /// type events and filter those further based on their message type. - /// - /// Virtual timeline items will still be provided and the - /// `default_event_filter` will be applied before everything else. - /// - /// # Arguments - /// - /// * `internal_id_prefix` - An optional String that will be prepended to - /// all the timeline item's internal IDs, making it possible to - /// distinguish different timeline instances from each other. - /// - /// * `allowed_message_types` - A list of `RoomMessageEventMessageType` that - /// will be allowed to appear in the timeline - pub async fn message_filtered_timeline( - &self, - internal_id_prefix: Option, - allowed_message_types: Vec, - date_divider_mode: DateDividerMode, + configuration: TimelineConfiguration, ) -> Result, ClientError> { let mut builder = matrix_sdk_ui::timeline::Timeline::builder(&self.inner); - if let Some(internal_id_prefix) = internal_id_prefix { + builder = builder.with_focus(configuration.focus.try_into()?); + + if let AllowedMessageTypes::Only { types } = configuration.allowed_message_types { + builder = builder.event_filter(move |event, room_version_id| { + default_event_filter(event, room_version_id) + && match event { + AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() { + Some(AnyMessageLikeEventContent::RoomMessage(content)) => { + types.contains(&content.msgtype.into()) + } + _ => false, + }, + _ => false, + } + }); + } + + if let Some(internal_id_prefix) = configuration.internal_id_prefix { builder = builder.with_internal_id_prefix(internal_id_prefix); } - builder = builder.with_date_divider_mode(date_divider_mode.into()); - - builder = builder.event_filter(move |event, room_version_id| { - default_event_filter(event, room_version_id) - && match event { - AnySyncTimelineEvent::MessageLike(msg) => match msg.original_content() { - Some(AnyMessageLikeEventContent::RoomMessage(content)) => { - allowed_message_types.contains(&content.msgtype.into()) - } - _ => false, - }, - _ => false, - } - }); + builder = builder.with_date_divider_mode(configuration.date_divider_mode.into()); let timeline = builder.build().await?; Ok(Timeline::new(timeline)) } + pub fn id(&self) -> String { + self.inner.room_id().to_string() + } + pub fn is_encrypted(&self) -> Result { Ok(RUNTIME.block_on(self.inner.is_encrypted())?) } @@ -1042,6 +970,75 @@ impl Room { let visibility = self.inner.privacy_settings().get_room_visibility().await?; Ok(visibility.into()) } + + /// Start the current users live location share in the room. + pub async fn start_live_location_share(&self, duration_millis: u64) -> Result<(), ClientError> { + self.inner.start_live_location_share(duration_millis, None).await?; + Ok(()) + } + + /// Stop the current users live location share in the room. + pub async fn stop_live_location_share(&self) -> Result<(), ClientError> { + self.inner.stop_live_location_share().await.expect("Unable to stop live location share"); + Ok(()) + } + + /// Send the current users live location beacon in the room. + pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> { + self.inner + .send_location_beacon(geo_uri) + .await + .expect("Unable to send live location beacon"); + Ok(()) + } + + /// Subscribes to live location shares in this room, using a `listener` to + /// be notified of the changes. + /// + /// The current live location shares will be emitted immediately when + /// subscribing, along with a [`TaskHandle`] to cancel the subscription. + pub fn subscribe_to_live_location_shares( + self: Arc, + listener: Box, + ) -> Arc { + let room = self.inner.clone(); + + Arc::new(TaskHandle::new(RUNTIME.spawn(async move { + let subscription = room.observe_live_location_shares(); + let mut stream = subscription.subscribe(); + let mut pinned_stream = pin!(stream); + + while let Some(event) = pinned_stream.next().await { + let last_location = LocationContent { + body: "".to_owned(), + geo_uri: event.last_location.location.uri.clone().to_string(), + description: None, + zoom_level: None, + asset: None, + }; + + let Some(beacon_info) = event.beacon_info else { + warn!("Live location share is missing the associated beacon_info state, skipping event."); + continue; + }; + + listener.call(vec![LiveLocationShare { + last_location: LastLocation { + location: last_location, + ts: event.last_location.ts.0.into(), + }, + is_live: beacon_info.is_live(), + user_id: event.user_id.to_string(), + }]) + } + }))) + } +} + +/// A listener for receiving new live location shares in a room. +#[matrix_sdk_ffi_macros::export(callback_interface)] +pub trait LiveLocationShareListener: Sync + Send { + fn call(&self, live_location_shares: Vec); } impl From for KnockRequest { diff --git a/bindings/matrix-sdk-ffi/src/timeline/configuration.rs b/bindings/matrix-sdk-ffi/src/timeline/configuration.rs new file mode 100644 index 000000000..9b20f99e9 --- /dev/null +++ b/bindings/matrix-sdk-ffi/src/timeline/configuration.rs @@ -0,0 +1,85 @@ +use ruma::EventId; + +use super::FocusEventError; +use crate::{error::ClientError, event::RoomMessageEventMessageType}; + +#[derive(uniffi::Enum)] +pub enum TimelineFocus { + Live, + Event { event_id: String, num_context_events: u16 }, + PinnedEvents { max_events_to_load: u16, max_concurrent_requests: u16 }, +} + +impl TryFrom for matrix_sdk_ui::timeline::TimelineFocus { + type Error = ClientError; + + fn try_from( + value: TimelineFocus, + ) -> Result { + match value { + TimelineFocus::Live => Ok(Self::Live), + TimelineFocus::Event { event_id, num_context_events } => { + let parsed_event_id = + EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId { + event_id: event_id.clone(), + err: err.to_string(), + })?; + + Ok(Self::Event { target: parsed_event_id, num_context_events }) + } + TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => { + Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests }) + } + } + } +} + +/// Changes how date dividers get inserted, either in between each day or in +/// between each month +#[derive(uniffi::Enum)] +pub enum DateDividerMode { + Daily, + Monthly, +} + +impl From for matrix_sdk_ui::timeline::DateDividerMode { + fn from(value: DateDividerMode) -> Self { + match value { + DateDividerMode::Daily => Self::Daily, + DateDividerMode::Monthly => Self::Monthly, + } + } +} + +#[derive(uniffi::Enum)] +pub enum AllowedMessageTypes { + All, + Only { types: Vec }, +} + +/// Various options used to configure the timeline's behavior. +/// +/// # Arguments +/// +/// * `internal_id_prefix` - +/// +/// * `allowed_message_types` - +/// +/// * `date_divider_mode` - +#[derive(uniffi::Record)] +pub struct TimelineConfiguration { + /// What should the timeline focus on? + pub focus: TimelineFocus, + + /// A list of [`RoomMessageEventMessageType`] that will be allowed to appear + /// in the timeline + pub allowed_message_types: AllowedMessageTypes, + + /// An optional String that will be prepended to + /// all the timeline item's internal IDs, making it possible to + /// distinguish different timeline instances from each other. + pub internal_id_prefix: Option, + + /// How often to insert date dividers + pub date_divider_mode: DateDividerMode, +} diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index 3520ce64c..5f7a0484b 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -19,8 +19,6 @@ use as_variant::as_variant; use content::{InReplyToDetails, RepliedToEventDetails}; use eyeball_im::VectorDiff; use futures_util::{pin_mut, StreamExt as _}; -#[cfg(doc)] -use matrix_sdk::crypto::CollectStrategy; use matrix_sdk::{ attachment::{ AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, @@ -63,8 +61,6 @@ use tracing::{error, warn}; use uuid::Uuid; use self::content::{Reaction, ReactionSenderData, TimelineItemContent}; -#[cfg(doc)] -use crate::client_builder::ClientBuilder; use crate::{ client::ProgressWatcher, error::{ClientError, RoomError}, @@ -79,6 +75,7 @@ use crate::{ RUNTIME, }; +pub mod configuration; mod content; pub use content::MessageContent; @@ -215,7 +212,7 @@ pub struct UploadParameters { #[matrix_sdk_ffi_macros::export] impl Timeline { pub async fn add_listener(&self, listener: Box) -> Arc { - let (timeline_items, timeline_stream) = self.inner.subscribe_batched().await; + let (timeline_items, timeline_stream) = self.inner.subscribe().await; Arc::new(TaskHandle::new(RUNTIME.spawn(async move { pin_mut!(timeline_stream); @@ -270,16 +267,16 @@ impl Timeline { /// Paginate backwards, whether we are in focused mode or in live mode. /// - /// Returns whether we hit the end of the timeline or not. + /// Returns whether we hit the start of the timeline or not. pub async fn paginate_backwards(&self, num_events: u16) -> Result { Ok(self.inner.paginate_backwards(num_events).await?) } - /// Paginate forwards, when in focused mode. + /// Paginate forwards, whether we are in focused mode or in live mode. /// /// Returns whether we hit the end of the timeline or not. - pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result { - Ok(self.inner.focused_paginate_forwards(num_events).await?) + pub async fn paginate_forwards(&self, num_events: u16) -> Result { + Ok(self.inner.paginate_forwards(num_events).await?) } pub async fn send_read_receipt( @@ -1321,21 +1318,8 @@ impl LazyTimelineItemProvider { fn get_send_handle(&self) -> Option> { self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle))) } -} -/// Changes how date dividers get inserted, either in between each day or in -/// between each month -#[derive(Debug, Clone, uniffi::Enum)] -pub enum DateDividerMode { - Daily, - Monthly, -} - -impl From for matrix_sdk_ui::timeline::DateDividerMode { - fn from(value: DateDividerMode) -> Self { - match value { - DateDividerMode::Daily => Self::Daily, - DateDividerMode::Monthly => Self::Monthly, - } + fn contains_only_emojis(&self) -> bool { + self.0.contains_only_emojis() } } diff --git a/crates/matrix-sdk-base/CHANGELOG.md b/crates/matrix-sdk-base/CHANGELOG.md index 7182f9acc..f113e63eb 100644 --- a/crates/matrix-sdk-base/CHANGELOG.md +++ b/crates/matrix-sdk-base/CHANGELOG.md @@ -30,6 +30,11 @@ All notable changes to this project will be documented in this file. cached value from the previous successful computation. If you need a sync variant, consider using `Room::cached_display_name()`. ([#4470](https://github.com/matrix-org/matrix-rust-sdk/pull/4470)) +- [**breaking**]: The reexported types `SyncTimelineEvent` and `TimelineEvent` + have been fused into a single type `TimelineEvent`, and its field + `push_actions` has been made `Option`al (it is set to `None` when we couldn't + compute the push actions, because we lacked some information). + ([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568)) ## [0.9.0] - 2024-12-18 diff --git a/crates/matrix-sdk-base/src/client.rs b/crates/matrix-sdk-base/src/client.rs index 3bc630340..4a910a3ae 100644 --- a/crates/matrix-sdk-base/src/client.rs +++ b/crates/matrix-sdk-base/src/client.rs @@ -68,7 +68,7 @@ use crate::latest_event::{is_suitable_for_latest_event, LatestEvent, PossibleLat #[cfg(feature = "e2e-encryption")] use crate::RoomMemberships; use crate::{ - deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, SyncTimelineEvent}, + deserialized_responses::{DisplayName, RawAnySyncOrStrippedTimelineEvent, TimelineEvent}, error::{Error, Result}, event_cache::store::EventCacheStoreLock, response_processors::AccountDataProcessor, @@ -347,9 +347,9 @@ impl BaseClient { Ok(()) } - /// Attempt to decrypt the given raw event into a `SyncTimelineEvent`. + /// Attempt to decrypt the given raw event into a [`TimelineEvent`]. /// - /// In the case of a decryption error, returns a `SyncTimelineEvent` + /// In the case of a decryption error, returns a [`TimelineEvent`] /// representing the decryption error; in the case of problems with our /// application, returns `Err`. /// @@ -359,7 +359,7 @@ impl BaseClient { &self, event: &Raw, room_id: &RoomId, - ) -> Result> { + ) -> Result> { let olm = self.olm_machine().await; let Some(olm) = olm.as_ref() else { return Ok(None) }; @@ -372,7 +372,7 @@ impl BaseClient { .await? { RoomEventDecryptionResult::Decrypted(decrypted) => { - let event: SyncTimelineEvent = decrypted.into(); + let event: TimelineEvent = decrypted.into(); if let Ok(AnySyncTimelineEvent::MessageLike(e)) = event.raw().deserialize() { match &e { @@ -394,7 +394,7 @@ impl BaseClient { event } RoomEventDecryptionResult::UnableToDecrypt(utd_info) => { - SyncTimelineEvent::new_utd_event(event.clone(), utd_info) + TimelineEvent::new_utd_event(event.clone(), utd_info) } }; @@ -423,7 +423,7 @@ impl BaseClient { for raw_event in events { // Start by assuming we have a plaintext event. We'll replace it with a // decrypted or UTD event below if necessary. - let mut event = SyncTimelineEvent::new(raw_event); + let mut event = TimelineEvent::new(raw_event); match event.raw().deserialize() { Ok(e) => { @@ -535,7 +535,7 @@ impl BaseClient { }, ); } - event.push_actions = actions.to_owned(); + event.push_actions = Some(actions.to_owned()); } } Err(e) => { diff --git a/crates/matrix-sdk-base/src/event_cache/mod.rs b/crates/matrix-sdk-base/src/event_cache/mod.rs index 0b4a80a4d..fb8f2bcf2 100644 --- a/crates/matrix-sdk-base/src/event_cache/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/mod.rs @@ -14,12 +14,12 @@ //! Event cache store and common types shared with `matrix_sdk::event_cache`. -use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_common::deserialized_responses::TimelineEvent; pub mod store; /// The kind of event the event storage holds. -pub type Event = SyncTimelineEvent; +pub type Event = TimelineEvent; /// The kind of gap the event storage holds. #[derive(Clone, Debug)] diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index b710b3b6d..c694933be 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -18,7 +18,7 @@ use assert_matches::assert_matches; use async_trait::async_trait; use matrix_sdk_common::{ deserialized_responses::{ - AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, SyncTimelineEvent, TimelineEventKind, + AlgorithmInfo, DecryptedRoomEvent, EncryptionInfo, TimelineEvent, TimelineEventKind, VerificationState, }, linked_chunk::{ @@ -42,7 +42,7 @@ use crate::{ /// correctly stores event data. /// /// Keep in sync with [`check_test_event`]. -pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent { +pub fn make_test_event(room_id: &RoomId, content: &str) -> TimelineEvent { let encryption_info = EncryptionInfo { sender: (*ALICE).into(), sender_device: None, @@ -60,13 +60,13 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent { .into_raw_timeline() .cast(); - SyncTimelineEvent { + TimelineEvent { kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info: None, }), - push_actions: vec![Action::Notify], + push_actions: Some(vec![Action::Notify]), } } @@ -75,9 +75,9 @@ pub fn make_test_event(room_id: &RoomId, content: &str) -> SyncTimelineEvent { /// /// Keep in sync with [`make_test_event`]. #[track_caller] -pub fn check_test_event(event: &SyncTimelineEvent, text: &str) { +pub fn check_test_event(event: &TimelineEvent, text: &str) { // Check push actions. - let actions = &event.push_actions; + let actions = event.push_actions.as_ref().unwrap(); assert_eq!(actions.len(), 1); assert_matches!(&actions[0], Action::Notify); diff --git a/crates/matrix-sdk-base/src/latest_event.rs b/crates/matrix-sdk-base/src/latest_event.rs index d610ec90f..92fc77a82 100644 --- a/crates/matrix-sdk-base/src/latest_event.rs +++ b/crates/matrix-sdk-base/src/latest_event.rs @@ -1,7 +1,7 @@ //! Utilities for working with events to decide whether they are suitable for //! use as a [crate::Room::latest_event]. -use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_common::deserialized_responses::TimelineEvent; #[cfg(feature = "e2e-encryption")] use ruma::{ events::{ @@ -164,7 +164,7 @@ pub fn is_suitable_for_latest_event<'a>( #[derive(Clone, Debug, Serialize)] pub struct LatestEvent { /// The actual event. - event: SyncTimelineEvent, + event: TimelineEvent, /// The member profile of the event' sender. #[serde(skip_serializing_if = "Option::is_none")] @@ -178,7 +178,7 @@ pub struct LatestEvent { #[derive(Deserialize)] struct SerializedLatestEvent { /// The actual event. - event: SyncTimelineEvent, + event: TimelineEvent, /// The member profile of the event' sender. #[serde(skip_serializing_if = "Option::is_none")] @@ -211,7 +211,7 @@ impl<'de> Deserialize<'de> for LatestEvent { Err(err) => variant_errors.push(err), } - match serde_json::from_str::(raw.get()) { + match serde_json::from_str::(raw.get()) { Ok(value) => { return Ok(LatestEvent { event: value, @@ -230,13 +230,13 @@ impl<'de> Deserialize<'de> for LatestEvent { impl LatestEvent { /// Create a new [`LatestEvent`] without the sender's profile. - pub fn new(event: SyncTimelineEvent) -> Self { + pub fn new(event: TimelineEvent) -> Self { Self { event, sender_profile: None, sender_name_is_ambiguous: None } } /// Create a new [`LatestEvent`] with maybe the sender's profile. pub fn new_with_sender_details( - event: SyncTimelineEvent, + event: TimelineEvent, sender_profile: Option, sender_name_is_ambiguous: Option, ) -> Self { @@ -244,17 +244,17 @@ impl LatestEvent { } /// Transform [`Self`] into an event. - pub fn into_event(self) -> SyncTimelineEvent { + pub fn into_event(self) -> TimelineEvent { self.event } /// Get a reference to the event. - pub fn event(&self) -> &SyncTimelineEvent { + pub fn event(&self) -> &TimelineEvent { &self.event } /// Get a mutable reference to the event. - pub fn event_mut(&mut self) -> &mut SyncTimelineEvent { + pub fn event_mut(&mut self) -> &mut TimelineEvent { &mut self.event } @@ -301,7 +301,7 @@ mod tests { use assert_matches::assert_matches; #[cfg(feature = "e2e-encryption")] use assert_matches2::assert_let; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::deserialized_responses::TimelineEvent; use ruma::serde::Raw; #[cfg(feature = "e2e-encryption")] use ruma::{ @@ -596,7 +596,7 @@ mod tests { latest_event: LatestEvent, } - let event = SyncTimelineEvent::new( + let event = TimelineEvent::new( Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(), ); diff --git a/crates/matrix-sdk-base/src/read_receipts.rs b/crates/matrix-sdk-base/src/read_receipts.rs index 84046a9ac..ee4282e9d 100644 --- a/crates/matrix-sdk-base/src/read_receipts.rs +++ b/crates/matrix-sdk-base/src/read_receipts.rs @@ -123,7 +123,7 @@ use std::{ }; use eyeball_im::Vector; -use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer}; +use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer}; use ruma::{ events::{ poll::{start::PollStartEventContent, unstable_start::UnstablePollStartEventContent}, @@ -202,7 +202,7 @@ impl RoomReadReceipts { /// /// Returns whether a new event triggered a new unread/notification/mention. #[inline(always)] - fn process_event(&mut self, event: &SyncTimelineEvent, user_id: &UserId) { + fn process_event(&mut self, event: &TimelineEvent, user_id: &UserId) { if marks_as_unread(event.raw(), user_id) { self.num_unread += 1; } @@ -210,7 +210,11 @@ impl RoomReadReceipts { let mut has_notify = false; let mut has_mention = false; - for action in &event.push_actions { + let Some(actions) = event.push_actions.as_ref() else { + return; + }; + + for action in actions.iter() { if !has_notify && action.should_notify() { self.num_notifications += 1; has_notify = true; @@ -236,7 +240,7 @@ impl RoomReadReceipts { &mut self, receipt_event_id: &EventId, user_id: &UserId, - events: impl IntoIterator, + events: impl IntoIterator, ) -> bool { let mut counting_receipts = false; @@ -269,11 +273,11 @@ impl RoomReadReceipts { pub trait PreviousEventsProvider: Send + Sync { /// Returns the list of known timeline events, in sync order, for the given /// room. - fn for_room(&self, room_id: &RoomId) -> Vector; + fn for_room(&self, room_id: &RoomId) -> Vector; } impl PreviousEventsProvider for () { - fn for_room(&self, _: &RoomId) -> Vector { + fn for_room(&self, _: &RoomId) -> Vector { Vector::new() } } @@ -292,7 +296,7 @@ struct ReceiptSelector { impl ReceiptSelector { fn new( - all_events: &Vector, + all_events: &Vector, latest_active_receipt_event: Option<&EventId>, ) -> Self { let event_id_to_pos = Self::create_sync_index(all_events.iter()); @@ -310,7 +314,7 @@ impl ReceiptSelector { /// Create a mapping of `event_id` -> sync order for all events that have an /// `event_id`. fn create_sync_index<'a>( - events: impl Iterator + 'a, + events: impl Iterator + 'a, ) -> BTreeMap { // TODO: this should be cached and incrementally updated. BTreeMap::from_iter( @@ -405,7 +409,7 @@ impl ReceiptSelector { /// Try to match an implicit receipt, that is, the one we get for events we /// sent ourselves. #[instrument(skip_all)] - fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[SyncTimelineEvent]) { + fn try_match_implicit(&mut self, user_id: &UserId, new_events: &[TimelineEvent]) { for ev in new_events { // Get the `sender` field, if any, or skip this event. let Ok(Some(sender)) = ev.raw().get_field::("sender") else { continue }; @@ -432,8 +436,8 @@ impl ReceiptSelector { /// Returns true if there's an event common to both groups of events, based on /// their event id. fn events_intersects<'a>( - previous_events: impl Iterator, - new_events: &[SyncTimelineEvent], + previous_events: impl Iterator, + new_events: &[TimelineEvent], ) -> bool { let previous_events_ids = BTreeSet::from_iter(previous_events.filter_map(|ev| ev.event_id())); new_events @@ -454,8 +458,8 @@ pub(crate) fn compute_unread_counts( user_id: &UserId, room_id: &RoomId, receipt_event: Option<&ReceiptEventContent>, - previous_events: Vector, - new_events: &[SyncTimelineEvent], + previous_events: Vector, + new_events: &[TimelineEvent], read_receipts: &mut RoomReadReceipts, ) { debug!(?read_receipts, "Starting."); @@ -620,7 +624,7 @@ mod tests { use std::{num::NonZeroUsize, ops::Not as _}; use eyeball_im::Vector; - use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer}; + use matrix_sdk_common::{deserialized_responses::TimelineEvent, ring_buffer::RingBuffer}; use matrix_sdk_test::event_factory::EventFactory; use ruma::{ event_id, @@ -720,13 +724,13 @@ mod tests { #[test] fn test_count_unread_and_mentions() { - fn make_event(user_id: &UserId, push_actions: Vec) -> SyncTimelineEvent { + fn make_event(user_id: &UserId, push_actions: Vec) -> TimelineEvent { let mut ev = EventFactory::new() .text_msg("A") .sender(user_id) .event_id(event_id!("$ida")) - .into_sync(); - ev.push_actions = push_actions; + .into_event(); + ev.push_actions = Some(push_actions); ev } @@ -801,7 +805,7 @@ mod tests { // When provided with one event, that's not the receipt event, we don't count // it. - fn make_event(event_id: &EventId) -> SyncTimelineEvent { + fn make_event(event_id: &EventId) -> TimelineEvent { EventFactory::new() .text_msg("A") .sender(user_id!("@bob:example.org")) @@ -915,8 +919,8 @@ mod tests { let mut previous_events = Vector::new(); let f = EventFactory::new(); - let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_sync(); - let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_sync(); + let ev1 = f.text_msg("A").sender(other_user_id).event_id(receipt_event_id).into_event(); + let ev2 = f.text_msg("A").sender(other_user_id).event_id(event_id!("$2")).into_event(); let receipt_event = f .read_receipts() @@ -940,7 +944,8 @@ mod tests { previous_events.push_back(ev1); previous_events.push_back(ev2); - let new_event = f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_sync(); + let new_event = + f.text_msg("A").sender(other_user_id).event_id(event_id!("$3")).into_event(); compute_unread_counts( user_id, room_id, @@ -954,7 +959,7 @@ mod tests { assert_eq!(read_receipts.num_unread, 2); } - fn make_test_events(user_id: &UserId) -> Vector { + fn make_test_events(user_id: &UserId) -> Vector { let f = EventFactory::new().sender(user_id); let ev1 = f.text_msg("With the lights out, it's less dangerous").event_id(event_id!("$1")); let ev2 = f.text_msg("Here we are now, entertain us").event_id(event_id!("$2")); @@ -1130,7 +1135,7 @@ mod tests { let events = make_test_events(uid); // An event with no id. - let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_sync(); + let ev6 = EventFactory::new().text_msg("yolo").sender(uid).no_event_id().into_event(); let index = ReceiptSelector::create_sync_index(events.iter().chain(&[ev6])); @@ -1197,8 +1202,8 @@ mod tests { fn test_receipt_selector_handle_pending_receipts_noop() { let sender = user_id!("@bob:example.org"); let f = EventFactory::new().sender(sender); - let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_sync(); - let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_sync(); + let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event(); + let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event(); let events: Vector<_> = vec![ev1, ev2].into(); { @@ -1233,8 +1238,8 @@ mod tests { fn test_receipt_selector_handle_pending_receipts_doesnt_match_known_events() { let sender = user_id!("@bob:example.org"); let f = EventFactory::new().sender(sender); - let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_sync(); - let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_sync(); + let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event(); + let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event(); let events: Vector<_> = vec![ev1, ev2].into(); { @@ -1270,8 +1275,8 @@ mod tests { fn test_receipt_selector_handle_pending_receipts_matches_known_events_no_initial() { let sender = user_id!("@bob:example.org"); let f = EventFactory::new().sender(sender); - let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_sync(); - let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_sync(); + let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event(); + let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event(); let events: Vector<_> = vec![ev1, ev2].into(); { @@ -1312,8 +1317,8 @@ mod tests { fn test_receipt_selector_handle_pending_receipts_matches_known_events_with_initial() { let sender = user_id!("@bob:example.org"); let f = EventFactory::new().sender(sender); - let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_sync(); - let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_sync(); + let ev1 = f.text_msg("yo").event_id(event_id!("$1")).into_event(); + let ev2 = f.text_msg("well?").event_id(event_id!("$2")).into_event(); let events: Vector<_> = vec![ev1, ev2].into(); { @@ -1491,10 +1496,10 @@ mod tests { f.text_msg("A mulatto, an albino") .sender(&myself) .event_id(event_id!("$6")) - .into_sync(), + .into_event(), ); events.push_back( - f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_sync(), + f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(), ); let mut selector = ReceiptSelector::new(&events, None); @@ -1520,15 +1525,15 @@ mod tests { f.text_msg("A mulatto, an albino") .sender(user_id) .event_id(event_id!("$6")) - .into_sync(), + .into_event(), ); // And others by Bob, events.push_back( - f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_sync(), + f.text_msg("A mosquito, my libido").sender(bob).event_id(event_id!("$7")).into_event(), ); events.push_back( - f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_sync(), + f.text_msg("A denial, a denial").sender(bob).event_id(event_id!("$8")).into_event(), ); let events: Vec<_> = events.into_iter().collect(); diff --git a/crates/matrix-sdk-base/src/rooms/normal.rs b/crates/matrix-sdk-base/src/rooms/normal.rs index f534ad399..316d64dcc 100644 --- a/crates/matrix-sdk-base/src/rooms/normal.rs +++ b/crates/matrix-sdk-base/src/rooms/normal.rs @@ -2164,7 +2164,7 @@ mod tests { }; use assign::assign; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_test::{ async_test, event_factory::EventFactory, @@ -2241,7 +2241,7 @@ mod tests { last_prev_batch: Some("pb".to_owned()), sync_info: SyncInfo::FullySynced, encryption_state_synced: true, - latest_event: Some(Box::new(LatestEvent::new(SyncTimelineEvent::new( + latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::new( Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(), )))), base_info: Box::new( @@ -3324,7 +3324,7 @@ mod tests { #[cfg(feature = "e2e-encryption")] fn make_latest_event(event_id: &str) -> Box { - Box::new(LatestEvent::new(SyncTimelineEvent::new( + Box::new(LatestEvent::new(TimelineEvent::new( Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(), ))) } diff --git a/crates/matrix-sdk-base/src/sliding_sync/mod.rs b/crates/matrix-sdk-base/src/sliding_sync/mod.rs index 29024555c..8ae81a41c 100644 --- a/crates/matrix-sdk-base/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk-base/src/sliding_sync/mod.rs @@ -21,7 +21,7 @@ use std::ops::Deref; use std::{borrow::Cow, collections::BTreeMap}; #[cfg(feature = "e2e-encryption")] -use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_common::deserialized_responses::TimelineEvent; use ruma::{ api::client::sync::sync_events::v3::{self, InvitedRoom, KnockedRoom}, events::{ @@ -690,7 +690,7 @@ impl BaseClient { async fn cache_latest_events( room: &Room, room_info: &mut RoomInfo, - events: &[SyncTimelineEvent], + events: &[TimelineEvent], changes: Option<&StateChanges>, store: Option<&Store>, ) { @@ -900,7 +900,7 @@ mod tests { use std::sync::{Arc, RwLock as SyncRwLock}; use assert_matches::assert_matches; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::deserialized_responses::TimelineEvent; #[cfg(feature = "e2e-encryption")] use matrix_sdk_common::{ deserialized_responses::{UnableToDecryptInfo, UnableToDecryptReason}, @@ -2606,7 +2606,7 @@ mod tests { } #[cfg(feature = "e2e-encryption")] - async fn choose_event_to_cache(events: &[SyncTimelineEvent]) -> Option { + async fn choose_event_to_cache(events: &[TimelineEvent]) -> Option { let room = make_room(); let mut room_info = room.clone_info(); cache_latest_events(&room, &mut room_info, events, None, None).await; @@ -2615,11 +2615,11 @@ mod tests { } #[cfg(feature = "e2e-encryption")] - fn rawev_id(event: SyncTimelineEvent) -> String { + fn rawev_id(event: TimelineEvent) -> String { event.event_id().unwrap().to_string() } - fn ev_id(event: Option) -> String { + fn ev_id(event: Option) -> String { event.unwrap().event_id().unwrap().to_string() } @@ -2629,7 +2629,7 @@ mod tests { } #[cfg(feature = "e2e-encryption")] - fn evs_ids(events: &[SyncTimelineEvent]) -> Vec { + fn evs_ids(events: &[TimelineEvent]) -> Vec { events.iter().map(|e| e.event_id().unwrap().to_string()).collect() } @@ -2661,13 +2661,13 @@ mod tests { } #[cfg(feature = "e2e-encryption")] - fn make_event(typ: &str, id: &str) -> SyncTimelineEvent { - SyncTimelineEvent::new(make_raw_event(typ, id)) + fn make_event(typ: &str, id: &str) -> TimelineEvent { + TimelineEvent::new(make_raw_event(typ, id)) } #[cfg(feature = "e2e-encryption")] - fn make_encrypted_event(id: &str) -> SyncTimelineEvent { - SyncTimelineEvent::new_utd_event( + fn make_encrypted_event(id: &str) -> TimelineEvent { + TimelineEvent::new_utd_event( Raw::from_json_string( json!({ "type": "m.room.encrypted", diff --git a/crates/matrix-sdk-base/src/store/migration_helpers.rs b/crates/matrix-sdk-base/src/store/migration_helpers.rs index e38be0770..b22456fd1 100644 --- a/crates/matrix-sdk-base/src/store/migration_helpers.rs +++ b/crates/matrix-sdk-base/src/store/migration_helpers.rs @@ -19,7 +19,7 @@ use std::{ sync::Arc, }; -use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_common::deserialized_responses::TimelineEvent; use ruma::{ events::{ direct::OwnedDirectUserIdentifier, @@ -76,7 +76,7 @@ pub struct RoomInfoV1 { sync_info: SyncInfo, #[serde(default = "encryption_state_default")] // see fn docs for why we use this default encryption_state_synced: bool, - latest_event: Option, + latest_event: Option, base_info: BaseRoomInfoV1, } diff --git a/crates/matrix-sdk-base/src/sync.rs b/crates/matrix-sdk-base/src/sync.rs index 824becb54..5b01e9010 100644 --- a/crates/matrix-sdk-base/src/sync.rs +++ b/crates/matrix-sdk-base/src/sync.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, fmt}; -use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::SyncTimelineEvent}; +use matrix_sdk_common::{debug::DebugRawEvent, deserialized_responses::TimelineEvent}; use ruma::{ api::client::sync::sync_events::{ v3::{InvitedRoom as InvitedRoomUpdate, KnockedRoom as KnockedRoomUpdate}, @@ -236,7 +236,7 @@ pub struct Timeline { pub prev_batch: Option, /// A list of events. - pub events: Vec, + pub events: Vec, } impl Timeline { diff --git a/crates/matrix-sdk-common/CHANGELOG.md b/crates/matrix-sdk-common/CHANGELOG.md index 7eedc0ab0..0af184d85 100644 --- a/crates/matrix-sdk-common/CHANGELOG.md +++ b/crates/matrix-sdk-common/CHANGELOG.md @@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - ReleaseDate +- [**breaking**]: `SyncTimelineEvent` and `TimelineEvent` have been fused into a single type + `TimelineEvent`, and its field `push_actions` has been made `Option`al (it is set to `None` when + we couldn't compute the push actions, because we lacked some information). + ([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568)) + ## [0.9.0] - 2024-12-18 ### Bug Fixes diff --git a/crates/matrix-sdk-common/src/deserialized_responses.rs b/crates/matrix-sdk-common/src/deserialized_responses.rs index cf27dae0d..0a25b5393 100644 --- a/crates/matrix-sdk-common/src/deserialized_responses.rs +++ b/crates/matrix-sdk-common/src/deserialized_responses.rs @@ -14,8 +14,10 @@ use std::{collections::BTreeMap, fmt}; +#[cfg(doc)] +use ruma::events::AnyTimelineEvent; use ruma::{ - events::{AnyMessageLikeEvent, AnySyncTimelineEvent, AnyTimelineEvent}, + events::{AnyMessageLikeEvent, AnySyncTimelineEvent}, push::Action, serde::{ AsRefStr, AsStrAsRefStr, DebugAsRefStr, DeserializeFromCowStr, FromString, JsonObject, Raw, @@ -311,13 +313,13 @@ pub struct EncryptionInfo { // // 🚨 Note about this type, please read! 🚨 // -// `SyncTimelineEvent` is heavily used across the SDK crates. In some cases, we +// `TimelineEvent` is heavily used across the SDK crates. In some cases, we // are reaching a [`recursion_limit`] when the compiler is trying to figure out -// if `SyncTimelineEvent` implements `Sync` when it's embedded in other types. +// if `TimelineEvent` implements `Sync` when it's embedded in other types. // // We want to help the compiler so that one doesn't need to increase the // `recursion_limit`. We stop the recursive check by (un)safely implement `Sync` -// and `Send` on `SyncTimelineEvent` directly. +// and `Send` on `TimelineEvent` directly. // // See // https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823 @@ -325,22 +327,24 @@ pub struct EncryptionInfo { // // [`recursion_limit`]: https://doc.rust-lang.org/reference/attributes/limits.html#the-recursion_limit-attribute #[derive(Clone, Debug, Serialize)] -pub struct SyncTimelineEvent { +pub struct TimelineEvent { /// The event itself, together with any information on decryption. pub kind: TimelineEventKind, /// The push actions associated with this event. - #[serde(skip_serializing_if = "Vec::is_empty")] - pub push_actions: Vec, + /// + /// If it's set to `None`, then it means we couldn't compute those actions. + #[serde(skip_serializing_if = "Option::is_none")] + pub push_actions: Option>, } // See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. #[cfg(not(feature = "test-send-sync"))] -unsafe impl Send for SyncTimelineEvent {} +unsafe impl Send for TimelineEvent {} // See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823. #[cfg(not(feature = "test-send-sync"))] -unsafe impl Sync for SyncTimelineEvent {} +unsafe impl Sync for TimelineEvent {} #[cfg(feature = "test-send-sync")] #[test] @@ -348,19 +352,19 @@ unsafe impl Sync for SyncTimelineEvent {} fn test_send_sync_for_sync_timeline_event() { fn assert_send_sync() {} - assert_send_sync::(); + assert_send_sync::(); } -impl SyncTimelineEvent { - /// Create a new `SyncTimelineEvent` from the given raw event. +impl TimelineEvent { + /// Create a new [`TimelineEvent`] from the given raw event. /// /// This is a convenience constructor for a plaintext event when you don't /// need to set `push_action`, for example inside a test. pub fn new(event: Raw) -> Self { - Self { kind: TimelineEventKind::PlainText { event }, push_actions: vec![] } + Self { kind: TimelineEventKind::PlainText { event }, push_actions: None } } - /// Create a new `SyncTimelineEvent` from the given raw event and push + /// Create a new [`TimelineEvent`] from the given raw event and push /// actions. /// /// This is a convenience constructor for a plaintext event, for example @@ -369,23 +373,23 @@ impl SyncTimelineEvent { event: Raw, push_actions: Vec, ) -> Self { - Self { kind: TimelineEventKind::PlainText { event }, push_actions } + Self { kind: TimelineEventKind::PlainText { event }, push_actions: Some(push_actions) } } - /// Create a new `SyncTimelineEvent` to represent the given decryption + /// Create a new [`TimelineEvent`] to represent the given decryption /// failure. pub fn new_utd_event(event: Raw, utd_info: UnableToDecryptInfo) -> Self { - Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: vec![] } + Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None } } - /// Get the event id of this `SyncTimelineEvent` if the event has any valid + /// Get the event id of this [`TimelineEvent`] if the event has any valid /// id. pub fn event_id(&self) -> Option { self.kind.event_id() } /// Returns a reference to the (potentially decrypted) Matrix event inside - /// this `TimelineEvent`. + /// this [`TimelineEvent`]. pub fn raw(&self) -> &Raw { self.kind.raw() } @@ -416,21 +420,14 @@ impl SyncTimelineEvent { } } -impl From for SyncTimelineEvent { - fn from(o: TimelineEvent) -> Self { - Self { kind: o.kind, push_actions: o.push_actions.unwrap_or_default() } - } -} - -impl From for SyncTimelineEvent { +impl From for TimelineEvent { fn from(decrypted: DecryptedRoomEvent) -> Self { - let timeline_event: TimelineEvent = decrypted.into(); - timeline_event.into() + Self { kind: TimelineEventKind::Decrypted(decrypted), push_actions: None } } } -impl<'de> Deserialize<'de> for SyncTimelineEvent { - /// Custom deserializer for [`SyncTimelineEvent`], to support older formats. +impl<'de> Deserialize<'de> for TimelineEvent { + /// Custom deserializer for [`TimelineEvent`], to support older formats. /// /// Ideally we might use an untagged enum and then convert from that; /// however, that doesn't work due to a [serde bug](https://github.com/serde-rs/json/issues/497). @@ -451,7 +448,7 @@ impl<'de> Deserialize<'de> for SyncTimelineEvent { let v0: SyncTimelineEventDeserializationHelperV0 = serde_json::from_value(Value::Object(value)).map_err(|e| { serde::de::Error::custom(format!( - "Unable to deserialize V0-format SyncTimelineEvent: {}", + "Unable to deserialize V0-format TimelineEvent: {}", e )) })?; @@ -462,7 +459,7 @@ impl<'de> Deserialize<'de> for SyncTimelineEvent { let v1: SyncTimelineEventDeserializationHelperV1 = serde_json::from_value(Value::Object(value)).map_err(|e| { serde::de::Error::custom(format!( - "Unable to deserialize V1-format SyncTimelineEvent: {}", + "Unable to deserialize V1-format TimelineEvent: {}", e )) })?; @@ -471,74 +468,7 @@ impl<'de> Deserialize<'de> for SyncTimelineEvent { } } -/// Represents a matrix room event that has been returned from a Matrix -/// client-server API endpoint such as `/messages`, after initial processing. -/// -/// The "initial processing" includes an attempt to decrypt encrypted events, so -/// the main thing this adds over [`AnyTimelineEvent`] is information on -/// encryption. -/// -/// Previously, this differed from [`SyncTimelineEvent`] by wrapping an -/// [`AnyTimelineEvent`] instead of an [`AnySyncTimelineEvent`], but nowadays -/// they are essentially identical, and one of them should probably be removed. -#[derive(Clone, Debug)] -pub struct TimelineEvent { - /// The event itself, together with any information on decryption. - pub kind: TimelineEventKind, - - /// The push actions associated with this event, if we had sufficient - /// context to compute them. - pub push_actions: Option>, -} - -impl TimelineEvent { - /// Create a new `TimelineEvent` from the given raw event. - /// - /// This is a convenience constructor for a plaintext event when you don't - /// need to set `push_action`, for example inside a test. - pub fn new(event: Raw) -> Self { - Self { - // This conversion is unproblematic since a `SyncTimelineEvent` is just a - // `TimelineEvent` without the `room_id`. By converting the raw value in - // this way, we simply cause the `room_id` field in the json to be - // ignored by a subsequent deserialization. - kind: TimelineEventKind::PlainText { event: event.cast() }, - push_actions: None, - } - } - - /// Create a new `TimelineEvent` to represent the given decryption failure. - pub fn new_utd_event(event: Raw, utd_info: UnableToDecryptInfo) -> Self { - Self { kind: TimelineEventKind::UnableToDecrypt { event, utd_info }, push_actions: None } - } - - /// Returns a reference to the (potentially decrypted) Matrix event inside - /// this `TimelineEvent`. - pub fn raw(&self) -> &Raw { - self.kind.raw() - } - - /// If the event was a decrypted event that was successfully decrypted, get - /// its encryption info. Otherwise, `None`. - pub fn encryption_info(&self) -> Option<&EncryptionInfo> { - self.kind.encryption_info() - } - - /// Takes ownership of this `TimelineEvent`, returning the (potentially - /// decrypted) Matrix event within. - pub fn into_raw(self) -> Raw { - self.kind.into_raw() - } -} - -impl From for TimelineEvent { - fn from(decrypted: DecryptedRoomEvent) -> Self { - Self { kind: TimelineEventKind::Decrypted(decrypted), push_actions: None } - } -} - -/// The event within a [`TimelineEvent`] or [`SyncTimelineEvent`], together with -/// encryption data. +/// The event within a [`TimelineEvent`], together with encryption data. #[derive(Clone, Serialize, Deserialize)] pub enum TimelineEventKind { /// A successfully-decrypted encrypted event. @@ -877,9 +807,9 @@ impl fmt::Debug for PrivOwnedStr { } } -/// Deserialization helper for [`SyncTimelineEvent`], for the modern format. +/// Deserialization helper for [`TimelineEvent`], for the modern format. /// -/// This has the exact same fields as [`SyncTimelineEvent`] itself, but has a +/// This has the exact same fields as [`TimelineEvent`] itself, but has a /// regular `Deserialize` implementation. #[derive(Debug, Deserialize)] struct SyncTimelineEventDeserializationHelperV1 { @@ -891,14 +821,14 @@ struct SyncTimelineEventDeserializationHelperV1 { push_actions: Vec, } -impl From for SyncTimelineEvent { +impl From for TimelineEvent { fn from(value: SyncTimelineEventDeserializationHelperV1) -> Self { let SyncTimelineEventDeserializationHelperV1 { kind, push_actions } = value; - SyncTimelineEvent { kind, push_actions } + TimelineEvent { kind, push_actions: Some(push_actions) } } } -/// Deserialization helper for [`SyncTimelineEvent`], for an older format. +/// Deserialization helper for [`TimelineEvent`], for an older format. #[derive(Deserialize)] struct SyncTimelineEventDeserializationHelperV0 { /// The actual event. @@ -919,7 +849,7 @@ struct SyncTimelineEventDeserializationHelperV0 { unsigned_encryption_info: Option>, } -impl From for SyncTimelineEvent { +impl From for TimelineEvent { fn from(value: SyncTimelineEventDeserializationHelperV0) -> Self { let SyncTimelineEventDeserializationHelperV0 { event, @@ -946,7 +876,7 @@ impl From for SyncTimelineEvent { None => TimelineEventKind::PlainText { event }, }; - SyncTimelineEvent { kind, push_actions } + TimelineEvent { kind, push_actions: Some(push_actions) } } } @@ -957,17 +887,15 @@ mod tests { use assert_matches::assert_matches; use insta::{assert_json_snapshot, with_settings}; use ruma::{ - device_id, event_id, - events::{room::message::RoomMessageEventContent, AnySyncTimelineEvent}, - serde::Raw, - user_id, DeviceKeyAlgorithm, + device_id, event_id, events::room::message::RoomMessageEventContent, serde::Raw, user_id, + DeviceKeyAlgorithm, }; use serde::Deserialize; use serde_json::json; use super::{ AlgorithmInfo, DecryptedRoomEvent, DeviceLinkProblem, EncryptionInfo, ShieldState, - ShieldStateCode, SyncTimelineEvent, TimelineEvent, TimelineEventKind, UnableToDecryptInfo, + ShieldStateCode, TimelineEvent, TimelineEventKind, UnableToDecryptInfo, UnableToDecryptReason, UnsignedDecryptionResult, UnsignedEventLocation, VerificationLevel, VerificationState, WithheldCode, }; @@ -985,7 +913,7 @@ mod tests { #[test] fn sync_timeline_debug_content() { - let room_event = SyncTimelineEvent::new(Raw::new(&example_event()).unwrap().cast()); + let room_event = TimelineEvent::new(Raw::new(&example_event()).unwrap().cast()); let debug_s = format!("{room_event:?}"); assert!( !debug_s.contains("secret"), @@ -993,18 +921,6 @@ mod tests { ); } - #[test] - fn room_event_to_sync_room_event() { - let room_event = TimelineEvent::new(Raw::new(&example_event()).unwrap().cast()); - let converted_room_event: SyncTimelineEvent = room_event.into(); - - let converted_event: AnySyncTimelineEvent = - converted_room_event.raw().deserialize().unwrap(); - - assert_eq!(converted_event.event_id(), "$xxxxx:example.org"); - assert_eq!(converted_event.sender(), "@carl:example.com"); - } - #[test] fn old_verification_state_to_new_migration() { #[derive(Deserialize)] @@ -1115,7 +1031,7 @@ mod tests { #[test] fn sync_timeline_event_serialisation() { - let room_event = SyncTimelineEvent { + let room_event = TimelineEvent { kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { event: Raw::new(&example_event()).unwrap().cast(), encryption_info: EncryptionInfo { @@ -1177,7 +1093,7 @@ mod tests { ); // And it can be properly deserialized from the new format. - let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + let event: TimelineEvent = serde_json::from_value(serialized).unwrap(); assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); assert_matches!( event.encryption_info().unwrap().algorithm_info, @@ -1206,7 +1122,7 @@ mod tests { "verification_state": "Verified", }, }); - let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + let event: TimelineEvent = serde_json::from_value(serialized).unwrap(); assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); assert_matches!( event.encryption_info().unwrap().algorithm_info, @@ -1239,7 +1155,7 @@ mod tests { "RelationsReplace": {"UnableToDecrypt": {"session_id": "xyz"}} } }); - let event: SyncTimelineEvent = serde_json::from_value(serialized).unwrap(); + let event: TimelineEvent = serde_json::from_value(serialized).unwrap(); assert_eq!(event.event_id(), Some(event_id!("$xxxxx:example.org").to_owned())); assert_matches!( event.encryption_info().unwrap().algorithm_info, @@ -1305,7 +1221,7 @@ mod tests { assert!(result.is_ok()); // should have migrated to the new format - let event: SyncTimelineEvent = result.unwrap(); + let event: TimelineEvent = result.unwrap(); assert_matches!( event.kind, TimelineEventKind::UnableToDecrypt { utd_info, .. }=> { @@ -1447,7 +1363,7 @@ mod tests { #[test] fn snapshot_test_sync_timeline_event() { - let room_event = SyncTimelineEvent { + let room_event = TimelineEvent { kind: TimelineEventKind::Decrypted(DecryptedRoomEvent { event: Raw::new(&example_event()).unwrap().cast(), encryption_info: EncryptionInfo { diff --git a/crates/matrix-sdk-common/src/lib.rs b/crates/matrix-sdk-common/src/lib.rs index 78c2b2d43..21ef01e59 100644 --- a/crates/matrix-sdk-common/src/lib.rs +++ b/crates/matrix-sdk-common/src/lib.rs @@ -28,6 +28,7 @@ pub mod failures_cache; pub mod linked_chunk; pub mod locks; pub mod ring_buffer; +pub mod sleep; pub mod store_locks; pub mod timeout; pub mod tracing_timer; diff --git a/crates/matrix-sdk-common/src/sleep.rs b/crates/matrix-sdk-common/src/sleep.rs new file mode 100644 index 000000000..2aebaa90f --- /dev/null +++ b/crates/matrix-sdk-common/src/sleep.rs @@ -0,0 +1,49 @@ +// Copyright 2024 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 std::time::Duration; + +/// Sleep for the specified duration. +/// +/// This is a cross-platform sleep implementation that works on both wasm32 and +/// non-wasm32 targets. +pub async fn sleep(duration: Duration) { + #[cfg(not(target_arch = "wasm32"))] + tokio::time::sleep(duration).await; + + #[cfg(target_arch = "wasm32")] + gloo_timers::future::TimeoutFuture::new(u32::try_from(duration.as_millis()).unwrap_or_else( + |_| { + tracing::error!("Sleep duration too long, sleeping for u32::MAX ms"); + u32::MAX + }, + )) + .await; +} + +#[cfg(test)] +mod tests { + use matrix_sdk_test_macros::async_test; + + use super::*; + + #[cfg(target_arch = "wasm32")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + #[async_test] + async fn test_sleep() { + // Just test that it doesn't panic + sleep(Duration::from_millis(1)).await; + } +} diff --git a/crates/matrix-sdk-common/src/store_locks.rs b/crates/matrix-sdk-common/src/store_locks.rs index 4b55d9c59..4d680771e 100644 --- a/crates/matrix-sdk-common/src/store_locks.rs +++ b/crates/matrix-sdk-common/src/store_locks.rs @@ -46,11 +46,12 @@ use std::{ time::Duration, }; -use tokio::{sync::Mutex, time::sleep}; +use tokio::sync::Mutex; use tracing::{debug, error, info, instrument, trace}; use crate::{ executor::{spawn, JoinHandle}, + sleep::sleep, SendOutsideWasm, }; diff --git a/crates/matrix-sdk-common/src/timeout.rs b/crates/matrix-sdk-common/src/timeout.rs index c56c19406..13417c41b 100644 --- a/crates/matrix-sdk-common/src/timeout.rs +++ b/crates/matrix-sdk-common/src/timeout.rs @@ -41,7 +41,7 @@ impl Error for ElapsedError {} /// an error. pub async fn timeout(future: F, duration: Duration) -> Result where - F: Future + Unpin, + F: Future, { #[cfg(not(target_arch = "wasm32"))] return tokio_timeout(duration, future).await.map_err(|_| ElapsedError(())); @@ -51,7 +51,7 @@ where let timeout_future = TimeoutFuture::new(u32::try_from(duration.as_millis()).expect("Overlong duration")); - match select(future, timeout_future).await { + match select(std::pin::pin!(future), timeout_future).await { Either::Left((res, _)) => Ok(res), Either::Right((_, _)) => Err(ElapsedError(())), } diff --git a/crates/matrix-sdk-crypto/CHANGELOG.md b/crates/matrix-sdk-crypto/CHANGELOG.md index 567c1df64..b7c7d84e2 100644 --- a/crates/matrix-sdk-crypto/CHANGELOG.md +++ b/crates/matrix-sdk-crypto/CHANGELOG.md @@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file. ### Features +- [**breaking**] `CollectStrategy::DeviceBasedStrategy` is now split into three + separate strategies (`AllDevices`, `ErrorOnVerifiedUserProblem`, + `OnlyTrustedDevices`), to make the behaviour clearer. + ([#4581](https://github.com/matrix-org/matrix-rust-sdk/pull/4581)) + - Accept stable identifier `sender_device_keys` for MSC4147 (Including device keys with Olm-encrypted events). ([#4420](https://github.com/matrix-org/matrix-rust-sdk/pull/4420)) diff --git a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs index 986450971..62ea9b08e 100644 --- a/crates/matrix-sdk-crypto/src/dehydrated_devices.rs +++ b/crates/matrix-sdk-crypto/src/dehydrated_devices.rs @@ -396,12 +396,20 @@ mod tests { use js_option::JsOption; use matrix_sdk_test::async_test; use ruma::{ - api::client::keys::get_keys::v3::Response as KeysQueryResponse, assign, - encryption::DeviceKeys, events::AnyToDeviceEvent, room_id, serde::Raw, user_id, DeviceId, - RoomId, TransactionId, UserId, + api::client::{ + dehydrated_device::put_dehydrated_device, + keys::get_keys::v3::Response as KeysQueryResponse, + }, + assign, + encryption::DeviceKeys, + events::AnyToDeviceEvent, + room_id, + serde::Raw, + user_id, DeviceId, RoomId, TransactionId, UserId, }; use crate::{ + dehydrated_devices::DehydratedDevice, machine::{ test_helpers::{create_session, get_prepared_machine_test_helper}, tests::to_device_requests_to_content, @@ -625,24 +633,18 @@ mod tests { let alice = get_olm_machine().await; let dehydrated_device = alice.dehydrated_devices().create().await.unwrap(); + let mut request = + legacy_dehydrated_device_keys_for_upload(&dehydrated_device, &pickle_key()).await; - let mut transaction = dehydrated_device.store.transaction().await; - let account = transaction.account().await.unwrap(); - account.generate_fallback_key_if_needed(); - - let (device_keys, mut one_time_keys, _fallback_keys) = account.keys_for_upload(); - let device_keys = device_keys.unwrap(); - - let device_data = account.legacy_dehydrate(&pickle_key().inner); - let device_id = account.device_id().to_owned(); - transaction.commit().await.unwrap(); - - let (key_id, one_time_key) = one_time_keys + let (key_id, one_time_key) = request + .one_time_keys .pop_first() .expect("The dehydrated device creation request should contain a one-time key"); + let device_id = request.device_id; + // Ensure that we know about the public keys of the dehydrated device. - receive_device_keys(&alice, user_id(), &device_id, device_keys.to_raw()).await; + receive_device_keys(&alice, user_id(), &device_id, request.device_keys).await; // Create a 1-to-1 Olm session with the dehydrated device. create_session(&alice, user_id(), &device_id, key_id, one_time_key).await; @@ -666,7 +668,7 @@ mod tests { // Rehydrate the device. let rehydrated = bob .dehydrated_devices() - .rehydrate(&pickle_key(), &device_id, device_data) + .rehydrate(&pickle_key(), &device_id, request.device_data) .await .expect("We should be able to rehydrate the device"); @@ -696,4 +698,35 @@ mod tests { "The session ids of the imported room key and the outbound group session should match" ); } + + /// Duplicates the behaviour of [`DehydratedDevice::keys_for_upload`], + /// except that it calls [`Account::legacy_dehydrate`] instead of + /// [`Account::dehydrate`]. + async fn legacy_dehydrated_device_keys_for_upload( + dehydrated_device: &DehydratedDevice, + pickle_key: &DehydratedDeviceKey, + ) -> put_dehydrated_device::unstable::Request { + let mut transaction = dehydrated_device.store.transaction().await; + let account = transaction.account().await.unwrap(); + account.generate_fallback_key_if_needed(); + + let (device_keys, one_time_keys, fallback_keys) = account.keys_for_upload(); + let mut device_keys = device_keys.unwrap(); + dehydrated_device + .store + .private_identity() + .lock() + .await + .sign_device_keys(&mut device_keys) + .await + .expect("Should be able to cross-sign a device"); + + let device_id = account.device_id().to_owned(); + let device_data = account.legacy_dehydrate(pickle_key.inner.as_ref()); + transaction.commit().await.unwrap(); + + assign!(put_dehydrated_device::unstable::Request::new(device_id, device_data, device_keys.to_raw()), { + one_time_keys, fallback_keys + }) + } } diff --git a/crates/matrix-sdk-crypto/src/error.rs b/crates/matrix-sdk-crypto/src/error.rs index 7a8ba63a8..6543d20d3 100644 --- a/crates/matrix-sdk-crypto/src/error.rs +++ b/crates/matrix-sdk-crypto/src/error.rs @@ -374,9 +374,7 @@ pub enum SetRoomSettingsError { pub enum SessionRecipientCollectionError { /// One or more verified users has one or more unsigned devices. /// - /// Happens only with [`CollectStrategy::DeviceBasedStrategy`] when - /// [`error_on_verified_user_problem`](`CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem`) - /// is true. + /// Happens only with [`CollectStrategy::ErrorOnVerifiedUserProblem`]. /// /// In order to resolve this, the caller can set the trust level of the /// affected devices to [`LocalTrust::Ignored`] or @@ -388,9 +386,8 @@ pub enum SessionRecipientCollectionError { /// One or more users was previously verified, but they have changed their /// identity. /// - /// Happens only with [`CollectStrategy::DeviceBasedStrategy`] when - /// [`error_on_verified_user_problem`](`CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem`) - /// is true, or with [`CollectStrategy::IdentityBasedStrategy`]. + /// Happens only with [`CollectStrategy::ErrorOnVerifiedUserProblem`] or + /// [`CollectStrategy::IdentityBasedStrategy`]. /// /// In order to resolve this, the user can: /// diff --git a/crates/matrix-sdk-crypto/src/machine/mod.rs b/crates/matrix-sdk-crypto/src/machine/mod.rs index fc79eea16..c33f169ab 100644 --- a/crates/matrix-sdk-crypto/src/machine/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/mod.rs @@ -852,9 +852,14 @@ impl OlmMachine { let mut decrypted = transaction.account().await?.decrypt_to_device_event(&self.inner.store, event).await?; - // Handle the decrypted event, e.g. fetch out Megolm sessions out of - // the event. - self.handle_decrypted_to_device_event(transaction.cache(), &mut decrypted, changes).await?; + // We ignore all to-device events from dehydrated devices - we should not + // receive any + if !self.to_device_event_is_from_dehydrated_device(&decrypted, &event.sender).await? { + // Handle the decrypted event, e.g. fetch out Megolm sessions out of + // the event. + self.handle_decrypted_to_device_event(transaction.cache(), &mut decrypted, changes) + .await?; + } Ok(decrypted) } @@ -1259,13 +1264,20 @@ impl OlmMachine { } } + /// Decrypt the supplied to-device event (if needed, and if we can) and + /// handle it. + /// + /// Return the same event, decrypted if possible and needed. + /// + /// If we can identify that this to-device event came from a dehydrated + /// device, this method does not process it, and returns `None`. #[instrument(skip_all, fields(sender, event_type, message_id))] async fn receive_to_device_event( &self, transaction: &mut StoreTransaction, changes: &mut Changes, mut raw_event: Raw, - ) -> Raw { + ) -> Option> { Self::record_message_id(&raw_event); let event: ToDeviceEvents = match raw_event.deserialize_as() { @@ -1274,7 +1286,7 @@ impl OlmMachine { // Skip invalid events. warn!("Received an invalid to-device event: {e}"); - return raw_event; + return Some(raw_event); } }; @@ -1299,10 +1311,30 @@ impl OlmMachine { } } - return raw_event; + return Some(raw_event); } }; + // We ignore all to-device events from dehydrated devices - we should not + // receive any + match self.to_device_event_is_from_dehydrated_device(&decrypted, &e.sender).await { + Ok(true) => { + warn!( + sender = ?e.sender, + session = ?decrypted.session, + "Received a to-device event from a dehydrated device. This is unexpected: ignoring event" + ); + return None; + } + Ok(false) => {} + Err(err) => { + error!( + error = ?err, + "Couldn't check whether event is from dehydrated device", + ); + } + } + // New sessions modify the account so we need to save that // one as well. match decrypted.session { @@ -1336,7 +1368,41 @@ impl OlmMachine { e => self.handle_to_device_event(changes, &e).await, } - raw_event + Some(raw_event) + } + + /// Decide whether a decrypted to-device event was sent from a dehydrated + /// device. + /// + /// This accepts an [`OlmDecryptionInfo`] because it deals with a decrypted + /// event. + async fn to_device_event_is_from_dehydrated_device( + &self, + decrypted: &OlmDecryptionInfo, + sender_user_id: &UserId, + ) -> OlmResult { + // Does the to-device message include device info? + if let Some(device_keys) = decrypted.result.event.sender_device_keys() { + // There is no need to check whether the device keys are signed correctly - any + // to-device message that claims to be from a dehydrated device is weird, so we + // will drop it. + + // Does the included device info say the device is dehydrated? + if device_keys.dehydrated.unwrap_or(false) { + return Ok(true); + } + // If not, fall through and check our existing list of devices + // below, just in case the sender is sending us incorrect + // information embedded in the to-device message, but we know + // better. + } + + // Do we already know about this device? + Ok(self + .store() + .get_device_from_curve_key(sender_user_id, decrypted.result.sender_key) + .await? + .is_some_and(|d| d.is_dehydrated())) } /// Handle a to-device and one-time key counts from a sync response. @@ -1377,6 +1443,14 @@ impl OlmMachine { Ok((events, room_key_updates)) } + /// Initial processing of the changes specified within a sync response. + /// + /// Returns the to-device events (decrypted where needed and where possible) + /// and the processed set of changes. + /// + /// If any of the to-device events in the supplied changes were sent from + /// dehydrated devices, these are not processed, and are omitted from + /// the returned list, as per MSC3814. pub(crate) async fn preprocess_sync_changes( &self, transaction: &mut StoreTransaction, @@ -1412,7 +1486,10 @@ impl OlmMachine { for raw_event in sync_changes.to_device_events { let raw_event = Box::pin(self.receive_to_device_event(transaction, &mut changes, raw_event)).await; - events.push(raw_event); + + if let Some(raw_event) = raw_event { + events.push(raw_event); + } } let changed_sessions = self diff --git a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs index fec64381f..588569932 100644 --- a/crates/matrix-sdk-crypto/src/machine/test_helpers.rs +++ b/crates/matrix-sdk-crypto/src/machine/test_helpers.rs @@ -34,7 +34,7 @@ use ruma::{ use serde_json::json; use crate::{ - store::Changes, + store::{Changes, MemoryStore}, types::{events::ToDeviceEvent, requests::AnyOutgoingRequest}, CrossSigningBootstrapRequests, DeviceData, OlmMachine, }; @@ -102,6 +102,23 @@ pub async fn get_machine_after_query_test_helper() -> (OlmMachine, OneTimeKeys) (machine, otk) } +pub async fn get_machine_pair_using_store( + alice: &UserId, + bob: &UserId, + use_fallback_key: bool, + alice_store: MemoryStore, + alice_device_id: &DeviceId, +) -> (OlmMachine, OlmMachine, OneTimeKeys) { + let (bob, otk) = get_prepared_machine_test_helper(bob, use_fallback_key).await; + + let alice = OlmMachine::with_store(alice, alice_device_id, alice_store, None) + .await + .expect("Failed to create OlmMachine from supplied store"); + + store_each_others_device_data(&alice, &bob).await; + (alice, bob, otk) +} + pub async fn get_machine_pair( alice: &UserId, bob: &UserId, @@ -112,12 +129,34 @@ pub async fn get_machine_pair( let alice_device = alice_device_id(); let alice = OlmMachine::new(alice, alice_device).await; - let alice_device = DeviceData::from_machine_test_helper(&alice).await.unwrap(); - let bob_device = DeviceData::from_machine_test_helper(&bob).await.unwrap(); + store_each_others_device_data(&alice, &bob).await; + (alice, bob, otk) +} + +/// Store alice's device data in bob's store and vice versa +async fn store_each_others_device_data(alice: &OlmMachine, bob: &OlmMachine) { + let alice_device = DeviceData::from_machine_test_helper(alice).await.unwrap(); + let bob_device = DeviceData::from_machine_test_helper(bob).await.unwrap(); alice.store().save_device_data(&[bob_device]).await.unwrap(); bob.store().save_device_data(&[alice_device]).await.unwrap(); +} - (alice, bob, otk) +/// Return a pair of [`OlmMachine`]s, with an olm session created on Alice's +/// side, but with no message yet sent. +/// +/// Create Alice's `OlmMachine` using the [`MemoryStore`] provided +pub async fn get_machine_pair_with_session_using_store( + alice: &UserId, + bob: &UserId, + use_fallback_key: bool, + alice_store: MemoryStore, + alice_device_id: &DeviceId, +) -> (OlmMachine, OlmMachine) { + let (alice, bob, one_time_keys) = + get_machine_pair_using_store(alice, bob, use_fallback_key, alice_store, alice_device_id) + .await; + + build_session_for_pair(alice, bob, one_time_keys).await } /// Return a pair of [`OlmMachine`]s, with an olm session created on Alice's @@ -127,8 +166,20 @@ pub async fn get_machine_pair_with_session( bob: &UserId, use_fallback_key: bool, ) -> (OlmMachine, OlmMachine) { - let (alice, bob, mut one_time_keys) = get_machine_pair(alice, bob, use_fallback_key).await; + let (alice, bob, one_time_keys) = get_machine_pair(alice, bob, use_fallback_key).await; + build_session_for_pair(alice, bob, one_time_keys).await +} + +/// Create a session for the two supplied Olm machines to communicate. +async fn build_session_for_pair( + alice: OlmMachine, + bob: OlmMachine, + mut one_time_keys: BTreeMap< + ruma::OwnedKeyId, + Raw, + >, +) -> (OlmMachine, OlmMachine) { let (device_key_id, one_time_key) = one_time_keys.pop_first().unwrap(); let one_time_keys = BTreeMap::from([( diff --git a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs index 09ea51072..8ab0b9eaa 100644 --- a/crates/matrix-sdk-crypto/src/machine/tests/mod.rs +++ b/crates/matrix-sdk-crypto/src/machine/tests/mod.rs @@ -38,7 +38,7 @@ use ruma::{ room_id, serde::Raw, uint, user_id, DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, - OneTimeKeyAlgorithm, TransactionId, UserId, + OneTimeKeyAlgorithm, RoomId, TransactionId, UserId, }; use serde_json::json; use vodozemac::{ @@ -48,17 +48,21 @@ use vodozemac::{ use super::CrossSigningBootstrapRequests; use crate::{ - error::EventError, + error::{EventError, OlmResult}, machine::{ test_helpers::{ get_machine_after_query_test_helper, get_machine_pair_with_session, + get_machine_pair_with_session_using_store, get_machine_pair_with_setup_sessions_test_helper, get_prepared_machine_test_helper, }, EncryptionSyncChanges, OlmMachine, }, olm::{BackedUpRoomKey, ExportedRoomKey, SenderData, VerifyJson}, session_manager::CollectStrategy, - store::{BackupDecryptionKey, Changes, CryptoStore, MemoryStore}, + store::{ + BackupDecryptionKey, Changes, CryptoStore, DeviceChanges, MemoryStore, PendingChanges, + RoomKeyInfo, + }, types::{ events::{ room::encrypted::{EncryptedToDeviceEvent, ToDeviceEncryptedEventContent}, @@ -70,7 +74,7 @@ use crate::{ }, utilities::json_convert, verification::tests::bob_id, - Account, DecryptionSettings, DeviceData, EncryptionSettings, MegolmError, OlmError, + Account, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, MegolmError, OlmError, RoomEventDecryptionResult, TrustRequirement, }; @@ -388,33 +392,10 @@ async fn test_missing_sessions_calculation() { #[async_test] async fn test_room_key_sharing() { let (alice, bob) = get_machine_pair_with_session(alice_id(), user_id(), false).await; - let room_id = room_id!("!test:example.org"); - let to_device_requests = alice - .share_room_key(room_id, iter::once(bob.user_id()), EncryptionSettings::default()) - .await - .unwrap(); - - let event = ToDeviceEvent::new( - alice.user_id().to_owned(), - to_device_requests_to_content(to_device_requests), - ); - let event = json_convert(&event).unwrap(); - - let alice_session = - alice.inner.group_session_manager.get_outbound_group_session(room_id).unwrap(); - - let (decrypted, room_key_updates) = bob - .receive_sync_changes(EncryptionSyncChanges { - to_device_events: vec![event], - changed_devices: &Default::default(), - one_time_keys_counts: &Default::default(), - unused_fallback_keys: None, - next_batch_token: None, - }) - .await - .unwrap(); + let (decrypted, room_key_updates) = + send_room_key_to_device(&alice, &bob, room_id).await.unwrap(); let event = decrypted[0].deserialize().unwrap(); @@ -425,6 +406,9 @@ async fn test_room_key_sharing() { panic!("expected RoomKeyEvent found {event:?}"); } + let alice_session = + alice.inner.group_session_manager.get_outbound_group_session(room_id).unwrap(); + let session = bob.store().get_inbound_group_session(room_id, alice_session.session_id()).await; assert!(session.unwrap().is_some()); @@ -434,6 +418,101 @@ async fn test_room_key_sharing() { assert_eq!(room_key_updates[0].session_id, alice_session.session_id()); } +#[async_test] +async fn test_to_device_messages_from_dehydrated_devices_are_ignored() { + // Given alice's device is dehydrated + let (alice, bob) = create_dehydrated_machine_and_pair().await; + + // When we send a to-device message from alice to bob + // (Note: we send a room_key message, but it could be any to-device message.) + let room_id = room_id!("!test:example.org"); + let (decrypted, room_key_updates) = + send_room_key_to_device(&alice, &bob, room_id).await.unwrap(); + + // Then the to-device message was discarded, because it was from a dehydrated + // device + assert!(decrypted.is_empty()); + + // And the room key was not imported as a session + let alice_session = + alice.inner.group_session_manager.get_outbound_group_session(room_id).unwrap(); + let session = bob.store().get_inbound_group_session(room_id, alice_session.session_id()).await; + assert!(session.unwrap().is_none()); + + assert!(room_key_updates.is_empty()); +} + +/// "Send" a to-device message containing a room key from sender to receiver. +/// +/// (Actually constructs the JSON of a to-device message from `sender` and feeds +/// it in to `receiver`'s `receive_sync_changes` method. +/// +/// Returns the return value of `receive_sync_changes`, which is a tuple of +/// (decrypted to-device events, updated room keys). +async fn send_room_key_to_device( + sender: &OlmMachine, + receiver: &OlmMachine, + room_id: &RoomId, +) -> OlmResult<(Vec>, Vec)> { + let to_device_requests = sender + .share_room_key(room_id, iter::once(receiver.user_id()), EncryptionSettings::default()) + .await + .unwrap(); + + let event = ToDeviceEvent::new( + sender.user_id().to_owned(), + to_device_requests_to_content(to_device_requests), + ); + let event = json_convert(&event).unwrap(); + + receiver + .receive_sync_changes(EncryptionSyncChanges { + to_device_events: vec![event], + changed_devices: &Default::default(), + one_time_keys_counts: &Default::default(), + unused_fallback_keys: None, + next_batch_token: None, + }) + .await +} + +/// Create an alice, bob pair where alice's device is dehydrated. Create a +/// session for messages from alice to bob, and ensure bob knows alice's device +/// is dehydrated. +async fn create_dehydrated_machine_and_pair() -> (OlmMachine, OlmMachine) { + // Create a store holding info about an account that is linked to a dehydrated + // device. This should never happen in real life, so we have to poke the + // info into the store directly. + let alice_store = MemoryStore::new(); + let alice_dehydrated_account = Account::new_dehydrated(alice_id()); + let mut alice_static_account = alice_dehydrated_account.static_data().clone(); + alice_static_account.dehydrated = true; + let alice_device = DeviceData::from_account(&alice_dehydrated_account); + let alice_dehydrated_device_id = alice_device.device_id().to_owned(); + alice_device.set_trust_state(LocalTrust::Verified); + + let changes = Changes { + devices: DeviceChanges { new: vec![alice_device], ..Default::default() }, + ..Default::default() + }; + alice_store.save_changes(changes).await.expect("Failed to same changes to the store"); + alice_store + .save_pending_changes(PendingChanges { account: Some(alice_dehydrated_account) }) + .await + .expect("Failed to save pending changes to the store"); + + // Create the alice machine using the store we have made (and also create a + // normal bob machine) + get_machine_pair_with_session_using_store( + alice_id(), + user_id(), + false, + alice_store, + &alice_dehydrated_device_id, + ) + .await +} + #[async_test] async fn test_request_missing_secrets() { let (alice, _) = get_machine_pair_with_session(alice_id(), bob_id(), false).await; @@ -594,10 +673,7 @@ async fn test_withheld_unverified() { let encryption_settings = EncryptionSettings::default(); let encryption_settings = EncryptionSettings { - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: true, - error_on_verified_user_problem: false, - }, + sharing_strategy: CollectStrategy::OnlyTrustedDevices, ..encryption_settings }; @@ -851,7 +927,7 @@ async fn test_query_ratcheted_key() { .await .unwrap() .expect("should exist") - .set_trust_state(crate::LocalTrust::Verified); + .set_trust_state(LocalTrust::Verified); alice.create_outbound_group_session_with_defaults_test_helper(room_id).await.unwrap(); diff --git a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs index 95a5949de..9e079eb79 100644 --- a/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs +++ b/crates/matrix-sdk-crypto/src/olm/group_sessions/outbound.rs @@ -788,10 +788,7 @@ mod tests { let settings = EncryptionSettings::new( content.clone(), HistoryVisibility::Joined, - CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: false, - }, + CollectStrategy::AllDevices, ); assert_eq!(settings.rotation_period, ROTATION_PERIOD); @@ -803,10 +800,7 @@ mod tests { let settings = EncryptionSettings::new( content, HistoryVisibility::Shared, - CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: false, - }, + CollectStrategy::AllDevices, ); assert_eq!(settings.rotation_period, Duration::from_millis(3600)); diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs index 2e148471a..8c192e2dd 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/mod.rs @@ -1163,10 +1163,7 @@ mod tests { .any(|d| d.user_id() == user_id && d.device_id() == device_id)); let settings = EncryptionSettings { - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: true, - error_on_verified_user_problem: false, - }, + sharing_strategy: CollectStrategy::OnlyTrustedDevices, ..Default::default() }; let users = [user_id].into_iter(); @@ -1226,10 +1223,7 @@ mod tests { let users = keys_claim.one_time_keys.keys().map(Deref::deref); let settings = EncryptionSettings { - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: true, - error_on_verified_user_problem: false, - }, + sharing_strategy: CollectStrategy::OnlyTrustedDevices, ..Default::default() }; diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs index f3d6efeae..5c8f48ec5 100644 --- a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs @@ -35,40 +35,43 @@ use crate::{Device, UserIdentity}; /// Strategy to collect the devices that should receive room keys for the /// current discussion. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] +#[serde(from = "CollectStrategyDeserializationHelper")] pub enum CollectStrategy { - /// Device based sharing strategy. - DeviceBasedStrategy { - /// If `true`, devices that are not trusted will be excluded from the - /// conversation. A device is trusted if any of the following is true: - /// - It was manually marked as trusted. - /// - It was marked as verified via interactive verification. - /// - It is signed by its owner identity, and this identity has been - /// trusted via interactive verification. - /// - It is the current own device of the user. - only_allow_trusted_devices: bool, + /// Share with all (unblacklisted) devices. + #[default] + AllDevices, - /// If `true`, and a verified user has an unsigned device, key sharing - /// will fail with a - /// [`SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice`]. - /// - /// If `true`, and a verified user has replaced their identity, key - /// sharing will fail with a - /// [`SessionRecipientCollectionError::VerifiedUserChangedIdentity`]. - /// - /// Otherwise, keys are shared with unsigned devices as normal. - /// - /// Once the problematic devices are blacklisted or whitelisted the - /// caller can retry to share a second time. - #[serde(default)] - error_on_verified_user_problem: bool, - }, + /// Share with all devices, except errors for *verified* users cause sharing + /// to fail with an error. + /// + /// In this strategy, if a verified user has an unsigned device, + /// key sharing will fail with a + /// [`SessionRecipientCollectionError::VerifiedUserHasUnsignedDevice`]. + /// If a verified user has replaced their identity, key + /// sharing will fail with a + /// [`SessionRecipientCollectionError::VerifiedUserChangedIdentity`]. + /// + /// Otherwise, keys are shared with unsigned devices as normal. + /// + /// Once the problematic devices are blacklisted or whitelisted the + /// caller can retry to share a second time. + ErrorOnVerifiedUserProblem, /// Share based on identity. Only distribute to devices signed by their /// owner. If a user has no published identity he will not receive /// any room keys. IdentityBasedStrategy, + + /// Only share keys with devices that we "trust". A device is trusted if any + /// of the following is true: + /// - It was manually marked as trusted. + /// - It was marked as verified via interactive verification. + /// - It is signed by its owner identity, and this identity has been + /// trusted via interactive verification. + /// - It is the current own device of the user. + OnlyTrustedDevices, } impl CollectStrategy { @@ -78,11 +81,47 @@ impl CollectStrategy { } } -impl Default for CollectStrategy { - fn default() -> Self { - CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: false, +/// Deserialization helper for [`CollectStrategy`]. +#[derive(Deserialize)] +enum CollectStrategyDeserializationHelper { + /// `AllDevices`, `ErrorOnVerifiedUserProblem` and `OnlyTrustedDevices` used + /// to be implemented as a single strategy with flags. + DeviceBasedStrategy { + #[serde(default)] + error_on_verified_user_problem: bool, + + #[serde(default)] + only_allow_trusted_devices: bool, + }, + + AllDevices, + ErrorOnVerifiedUserProblem, + IdentityBasedStrategy, + OnlyTrustedDevices, +} + +impl From for CollectStrategy { + fn from(value: CollectStrategyDeserializationHelper) -> Self { + use CollectStrategyDeserializationHelper::*; + + match value { + DeviceBasedStrategy { + only_allow_trusted_devices: true, + error_on_verified_user_problem: _, + } => CollectStrategy::OnlyTrustedDevices, + DeviceBasedStrategy { + only_allow_trusted_devices: false, + error_on_verified_user_problem: true, + } => CollectStrategy::ErrorOnVerifiedUserProblem, + DeviceBasedStrategy { + only_allow_trusted_devices: false, + error_on_verified_user_problem: false, + } => CollectStrategy::AllDevices, + + AllDevices => CollectStrategy::AllDevices, + ErrorOnVerifiedUserProblem => CollectStrategy::ErrorOnVerifiedUserProblem, + IdentityBasedStrategy => CollectStrategy::IdentityBasedStrategy, + OnlyTrustedDevices => CollectStrategy::OnlyTrustedDevices, } } } @@ -93,7 +132,7 @@ impl Default for CollectStrategy { /// (`should_rotate`) and the list of users/devices that should receive /// (`devices`) or not the session, including withheld reason /// `withheld_devices`. -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) struct CollectRecipientsResult { /// If true the outbound group session should be rotated pub should_rotate: bool, @@ -118,8 +157,7 @@ pub(crate) async fn collect_session_recipients( outbound: &OutboundGroupSession, ) -> OlmResult { let users: BTreeSet<&UserId> = users.collect(); - let mut devices: BTreeMap> = Default::default(); - let mut withheld_devices: Vec<(DeviceData, WithheldCode)> = Default::default(); + let mut result = CollectRecipientsResult::default(); let mut verified_users_with_new_identities: Vec = Default::default(); trace!(?users, ?settings, "Calculating group session recipients"); @@ -144,82 +182,64 @@ pub(crate) async fn collect_session_recipients( // 4. The encryption algorithm changed. // // This is calculated in the following code and stored in this variable. - let mut should_rotate = user_left || visibility_changed || algorithm_changed; + result.should_rotate = user_left || visibility_changed || algorithm_changed; let own_identity = store.get_user_identity(store.user_id()).await?.and_then(|i| i.into_own()); // Get the recipient and withheld devices, based on the collection strategy. match settings.sharing_strategy { - CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices, - error_on_verified_user_problem, - } => { + CollectStrategy::AllDevices => { + for user_id in users { + trace!( + "CollectStrategy::AllDevices: Considering recipient devices for user {}", + user_id + ); + let user_devices = store.get_device_data_for_user_filtered(user_id).await?; + + let recipient_devices = + split_devices_for_user_for_all_devices_strategy(user_devices); + update_recipients_for_user(&mut result, outbound, user_id, recipient_devices); + } + } + CollectStrategy::ErrorOnVerifiedUserProblem => { let mut unsigned_devices_of_verified_users: BTreeMap> = Default::default(); for user_id in users { - trace!("Considering recipient devices for user {}", user_id); + trace!("CollectStrategy::ErrorOnVerifiedUserProblem: Considering recipient devices for user {}", user_id); let user_devices = store.get_device_data_for_user_filtered(user_id).await?; - // We only need the user identity if `only_allow_trusted_devices` or - // `error_on_verified_user_problem` is set. - let device_owner_identity = - if only_allow_trusted_devices || error_on_verified_user_problem { - store.get_user_identity(user_id).await? - } else { - None - }; + let device_owner_identity = store.get_user_identity(user_id).await?; - if error_on_verified_user_problem - && has_identity_verification_violation( - own_identity.as_ref(), - device_owner_identity.as_ref(), - ) - { + if has_identity_verification_violation( + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { verified_users_with_new_identities.push(user_id.to_owned()); // No point considering the individual devices of this user. continue; } - let recipient_devices = split_devices_for_user( - user_devices, - &own_identity, - &device_owner_identity, - only_allow_trusted_devices, - error_on_verified_user_problem, - ); - - // If `error_on_verified_user_problem` is set, then - // `unsigned_of_verified_user` may be populated. If so, add an entry to the - // list of users with unsigned devices. - if !recipient_devices.unsigned_of_verified_user.is_empty() { - unsigned_devices_of_verified_users.insert( - user_id.to_owned(), - recipient_devices - .unsigned_of_verified_user - .into_iter() - .map(|d| d.device_id().to_owned()) - .collect(), + let recipient_devices = + split_devices_for_user_for_error_on_verified_user_problem_strategy( + user_devices, + &own_identity, + &device_owner_identity, ); - } - // If we haven't already concluded that the session should be - // rotated for other reasons, we also need to check whether any - // of the devices in the session got deleted or blacklisted in the - // meantime. If so, we should also rotate the session. - if !should_rotate { - should_rotate = is_session_overshared_for_user( - outbound, - user_id, - &recipient_devices.allowed_devices, - ) + match recipient_devices { + ErrorOnVerifiedUserProblemResult::UnsignedDevicesOfVerifiedUser(devices) => { + unsigned_devices_of_verified_users.insert(user_id.to_owned(), devices); + } + ErrorOnVerifiedUserProblemResult::Devices(recipient_devices) => { + update_recipients_for_user( + &mut result, + outbound, + user_id, + recipient_devices, + ); + } } - - devices - .entry(user_id.to_owned()) - .or_default() - .extend(recipient_devices.allowed_devices); - withheld_devices.extend(recipient_devices.denied_devices_with_code); } // If `error_on_verified_user_problem` is set, then @@ -251,7 +271,7 @@ pub(crate) async fn collect_session_recipients( } for user_id in users { - trace!("Considering recipient devices for user {}", user_id); + trace!("CollectStrategy::IdentityBasedStrategy: Considering recipient devices for user {}", user_id); let user_devices = store.get_device_data_for_user_filtered(user_id).await?; let device_owner_identity = store.get_user_identity(user_id).await?; @@ -265,28 +285,28 @@ pub(crate) async fn collect_session_recipients( continue; } - let recipient_devices = split_recipients_withhelds_for_user_based_on_identity( + let recipient_devices = split_devices_for_user_for_identity_based_strategy( user_devices, &device_owner_identity, ); - // If we haven't already concluded that the session should be - // rotated for other reasons, we also need to check whether any - // of the devices in the session got deleted or blacklisted in the - // meantime. If so, we should also rotate the session. - if !should_rotate { - should_rotate = is_session_overshared_for_user( - outbound, - user_id, - &recipient_devices.allowed_devices, - ) - } + update_recipients_for_user(&mut result, outbound, user_id, recipient_devices); + } + } - devices - .entry(user_id.to_owned()) - .or_default() - .extend(recipient_devices.allowed_devices); - withheld_devices.extend(recipient_devices.denied_devices_with_code); + CollectStrategy::OnlyTrustedDevices => { + for user_id in users { + trace!("CollectStrategy::OnlyTrustedDevices: Considering recipient devices for user {}", user_id); + let user_devices = store.get_device_data_for_user_filtered(user_id).await?; + let device_owner_identity = store.get_user_identity(user_id).await?; + + let recipient_devices = split_devices_for_user_for_only_trusted_devices( + user_devices, + &own_identity, + &device_owner_identity, + ); + + update_recipients_for_user(&mut result, outbound, user_id, recipient_devices); } } } @@ -301,18 +321,43 @@ pub(crate) async fn collect_session_recipients( )); } - if should_rotate { + if result.should_rotate { debug!( - should_rotate, + result.should_rotate, user_left, visibility_changed, algorithm_changed, "Rotating room key to protect room history", ); } - trace!(should_rotate, "Done calculating group session recipients"); + trace!(result.should_rotate, "Done calculating group session recipients"); - Ok(CollectRecipientsResult { should_rotate, devices, withheld_devices }) + Ok(result) +} + +/// Update this [`CollectRecipientsResult`] with the device list for a specific +/// user. +fn update_recipients_for_user( + recipients: &mut CollectRecipientsResult, + outbound: &OutboundGroupSession, + user_id: &UserId, + recipient_devices: RecipientDevicesForUser, +) { + // If we haven't already concluded that the session should be + // rotated for other reasons, we also need to check whether any + // of the devices in the session got deleted or blacklisted in the + // meantime. If so, we should also rotate the session. + if !recipients.should_rotate { + recipients.should_rotate = + is_session_overshared_for_user(outbound, user_id, &recipient_devices.allowed_devices) + } + + recipients + .devices + .entry(user_id.to_owned()) + .or_default() + .extend(recipient_devices.allowed_devices); + recipients.withheld_devices.extend(recipient_devices.denied_devices_with_code); } /// Check if the session has been shared with a device belonging to the given @@ -368,83 +413,132 @@ fn is_session_overshared_for_user( should_rotate } -/// Result type for [`split_devices_for_user`]. +/// Result type for [`split_devices_for_user_for_all_devices_strategy`], +/// [`split_devices_for_user_for_error_on_verified_user_problem_strategy`], +/// [`split_devices_for_user_for_identity_based_strategy`], +/// [`split_devices_for_user_for_only_trusted_devices`]. +/// +/// A partitioning of the devices for a given user. #[derive(Default)] -struct DeviceBasedRecipientDevices { +struct RecipientDevicesForUser { /// Devices that should receive the room key. allowed_devices: Vec, /// Devices that should receive a withheld code. denied_devices_with_code: Vec<(DeviceData, WithheldCode)>, - /// Devices that should cause the transmission to fail, due to being an - /// unsigned device belonging to a verified user. Only populated by - /// [`split_devices_for_user`], when - /// `error_on_verified_user_problem` is set. - unsigned_of_verified_user: Vec, +} + +/// Result type for +/// [`split_devices_for_user_for_error_on_verified_user_problem_strategy`]. +enum ErrorOnVerifiedUserProblemResult { + /// We found devices that should cause the transmission to fail, due to + /// being an unsigned device belonging to a verified user. Only + /// populated when `error_on_verified_user_problem` is set. + UnsignedDevicesOfVerifiedUser(Vec), + + /// There were no unsigned devices of verified users. + Devices(RecipientDevicesForUser), } /// Partition the list of a user's devices according to whether they should -/// receive the key, for [`CollectStrategy::DeviceBasedStrategy`]. +/// receive the key, for [`CollectStrategy::AllDevices`]. +fn split_devices_for_user_for_all_devices_strategy( + user_devices: HashMap, +) -> RecipientDevicesForUser { + let (left, right) = user_devices.into_values().partition_map(|d| { + if d.is_blacklisted() { + Either::Right((d, WithheldCode::Blacklisted)) + } else { + Either::Left(d) + } + }); + + RecipientDevicesForUser { allowed_devices: left, denied_devices_with_code: right } +} + +/// Partition the list of a user's devices according to whether they should +/// receive the key, for [`CollectStrategy::ErrorOnVerifiedUserProblem`]. /// -/// We split the list into three buckets: +/// This function returns one of two values: /// -/// * the devices that should receive the room key. +/// * A list of the devices that should cause the transmission to fail due to +/// being unsigned. In this case, we don't bother to return the rest of the +/// devices, because we assume transmission will fail. /// -/// * the devices that should receive a withheld code. -/// -/// * If `error_on_verified_user_problem` is set, the devices that should cause -/// the transmission to fail due to being unsigned. (If -/// `error_on_verified_user_problem` is unset, these devices are otherwise -/// partitioned into `allowed_devices`.) -fn split_devices_for_user( +/// * Otherwise, returns a [`RecipientDevicesForUser`] which lists, separately, +/// the devices that should receive the room key, and those that should +/// receive a withheld code. +fn split_devices_for_user_for_error_on_verified_user_problem_strategy( user_devices: HashMap, own_identity: &Option, device_owner_identity: &Option, - only_allow_trusted_devices: bool, - error_on_verified_user_problem: bool, -) -> DeviceBasedRecipientDevices { - let mut recipient_devices: DeviceBasedRecipientDevices = Default::default(); +) -> ErrorOnVerifiedUserProblemResult { + let mut recipient_devices = RecipientDevicesForUser::default(); + + // We construct unsigned_devices_of_verified_users lazily, because chances are + // we won't need it. + let mut unsigned_devices_of_verified_users: Option> = None; + for d in user_devices.into_values() { - if d.is_blacklisted() { - recipient_devices.denied_devices_with_code.push((d, WithheldCode::Blacklisted)); - } else if d.local_trust_state() == LocalTrust::Ignored { - // Ignore the trust state of that device and share - recipient_devices.allowed_devices.push(d); - } else if only_allow_trusted_devices && !d.is_verified(own_identity, device_owner_identity) - { - recipient_devices.denied_devices_with_code.push((d, WithheldCode::Unverified)); - } else if error_on_verified_user_problem - && is_unsigned_device_of_verified_user( - own_identity.as_ref(), - device_owner_identity.as_ref(), - &d, - ) - { - recipient_devices.unsigned_of_verified_user.push(d) - } else { - recipient_devices.allowed_devices.push(d); + match handle_device_for_user_for_error_on_verified_user_problem_strategy( + &d, + own_identity.as_ref(), + device_owner_identity.as_ref(), + ) { + ErrorOnVerifiedUserProblemDeviceDecision::Ok => { + recipient_devices.allowed_devices.push(d) + } + ErrorOnVerifiedUserProblemDeviceDecision::Withhold(code) => { + recipient_devices.denied_devices_with_code.push((d, code)) + } + ErrorOnVerifiedUserProblemDeviceDecision::UnsignedOfVerified => { + unsigned_devices_of_verified_users + .get_or_insert_with(Vec::default) + .push(d.device_id().to_owned()) + } } } - recipient_devices + + if let Some(devices) = unsigned_devices_of_verified_users { + ErrorOnVerifiedUserProblemResult::UnsignedDevicesOfVerifiedUser(devices) + } else { + ErrorOnVerifiedUserProblemResult::Devices(recipient_devices) + } } -/// Result type for [`split_recipients_withhelds_for_user_based_on_identity`]. -#[derive(Default)] -struct IdentityBasedRecipientDevices { - /// Devices that should receive the room key. - allowed_devices: Vec, - /// Devices that should receive a withheld code. - denied_devices_with_code: Vec<(DeviceData, WithheldCode)>, +/// Result type for +/// [`handle_device_for_user_for_error_on_verified_user_problem_strategy`]. +enum ErrorOnVerifiedUserProblemDeviceDecision { + Ok, + Withhold(WithheldCode), + UnsignedOfVerified, } -fn split_recipients_withhelds_for_user_based_on_identity( +fn handle_device_for_user_for_error_on_verified_user_problem_strategy( + device: &DeviceData, + own_identity: Option<&OwnUserIdentityData>, + device_owner_identity: Option<&UserIdentityData>, +) -> ErrorOnVerifiedUserProblemDeviceDecision { + if device.is_blacklisted() { + ErrorOnVerifiedUserProblemDeviceDecision::Withhold(WithheldCode::Blacklisted) + } else if device.local_trust_state() == LocalTrust::Ignored { + // Ignore the trust state of that device and share + ErrorOnVerifiedUserProblemDeviceDecision::Ok + } else if is_unsigned_device_of_verified_user(own_identity, device_owner_identity, device) { + ErrorOnVerifiedUserProblemDeviceDecision::UnsignedOfVerified + } else { + ErrorOnVerifiedUserProblemDeviceDecision::Ok + } +} + +fn split_devices_for_user_for_identity_based_strategy( user_devices: HashMap, device_owner_identity: &Option, -) -> IdentityBasedRecipientDevices { +) -> RecipientDevicesForUser { match device_owner_identity { None => { // withheld all the users devices, we need to have an identity for this // distribution mode - IdentityBasedRecipientDevices { + RecipientDevicesForUser { allowed_devices: Vec::default(), denied_devices_with_code: user_devices .into_values() @@ -464,7 +558,7 @@ fn split_recipients_withhelds_for_user_based_on_identity( Either::Right((d, WithheldCode::Unverified)) } }); - IdentityBasedRecipientDevices { + RecipientDevicesForUser { allowed_devices: recipients, denied_devices_with_code: withheld_recipients, } @@ -472,6 +566,27 @@ fn split_recipients_withhelds_for_user_based_on_identity( } } +/// Partition the list of a user's devices according to whether they should +/// receive the key, for [`CollectStrategy::OnlyTrustedDevices`]. +fn split_devices_for_user_for_only_trusted_devices( + user_devices: HashMap, + own_identity: &Option, + device_owner_identity: &Option, +) -> RecipientDevicesForUser { + let (left, right) = user_devices.into_values().partition_map(|d| { + match ( + d.local_trust_state(), + d.is_cross_signing_trusted(own_identity, device_owner_identity), + ) { + (LocalTrust::BlackListed, _) => Either::Right((d, WithheldCode::Blacklisted)), + (LocalTrust::Ignored | LocalTrust::Verified, _) => Either::Left(d), + (LocalTrust::Unset, false) => Either::Right((d, WithheldCode::Unverified)), + (LocalTrust::Unset, true) => Either::Left(d), + } + }); + RecipientDevicesForUser { allowed_devices: left, denied_devices_with_code: right } +} + fn is_unsigned_device_of_verified_user( own_identity: Option<&OwnUserIdentityData>, device_owner_identity: Option<&UserIdentityData>, @@ -517,6 +632,7 @@ mod tests { use assert_matches::assert_matches; use assert_matches2::assert_let; + use insta::assert_snapshot; use matrix_sdk_common::deserialized_responses::WithheldCode; use matrix_sdk_test::{ async_test, test_json, @@ -579,17 +695,65 @@ mod tests { machine } + /// Assert that [`CollectStrategy::AllDevices`] retains the same + /// serialization format. + #[test] + fn test_serialize_device_based_strategy() { + let encryption_settings = all_devices_strategy_settings(); + let serialized = serde_json::to_string(&encryption_settings).unwrap(); + assert_snapshot!(serialized); + } + + /// [`CollectStrategy::AllDevices`] used to be known as + /// `DeviceBasedStrategy`. Check we can still deserialize the old + /// representation. + #[test] + fn test_deserialize_old_device_based_strategy() { + let settings: EncryptionSettings = serde_json::from_value(json!({ + "algorithm": "m.megolm.v1.aes-sha2", + "rotation_period":{"secs":604800,"nanos":0}, + "rotation_period_msgs":100, + "history_visibility":"shared", + "sharing_strategy":{"DeviceBasedStrategy":{"only_allow_trusted_devices":false,"error_on_verified_user_problem":false}}, + })).unwrap(); + assert_matches!(settings.sharing_strategy, CollectStrategy::AllDevices); + } + + /// [`CollectStrategy::ErrorOnVerifiedUserProblem`] used to be represented + /// as a variant on the former `DeviceBasedStrategy`. Check we can still + /// deserialize the old representation. + #[test] + fn test_deserialize_old_error_on_verified_user_problem() { + let settings: EncryptionSettings = serde_json::from_value(json!({ + "algorithm": "m.megolm.v1.aes-sha2", + "rotation_period":{"secs":604800,"nanos":0}, + "rotation_period_msgs":100, + "history_visibility":"shared", + "sharing_strategy":{"DeviceBasedStrategy":{"only_allow_trusted_devices":false,"error_on_verified_user_problem":true}}, + })).unwrap(); + assert_matches!(settings.sharing_strategy, CollectStrategy::ErrorOnVerifiedUserProblem); + } + + /// [`CollectStrategy::OnlyTrustedDevices`] used to be represented as a + /// variant on the former `DeviceBasedStrategy`. Check we can still + /// deserialize the old representation. + #[test] + fn test_deserialize_old_only_trusted_devices_strategy() { + let settings: EncryptionSettings = serde_json::from_value(json!({ + "algorithm": "m.megolm.v1.aes-sha2", + "rotation_period":{"secs":604800,"nanos":0}, + "rotation_period_msgs":100, + "history_visibility":"shared", + "sharing_strategy":{"DeviceBasedStrategy":{"only_allow_trusted_devices":true,"error_on_verified_user_problem":false}}, + })).unwrap(); + assert_matches!(settings.sharing_strategy, CollectStrategy::OnlyTrustedDevices); + } + #[async_test] async fn test_share_with_per_device_strategy_to_all() { let machine = set_up_test_machine().await; - let encryption_settings = EncryptionSettings { - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: false, - }, - ..Default::default() - }; + let encryption_settings = all_devices_strategy_settings(); let group_session = create_test_outbound_group_session(&machine, &encryption_settings); @@ -623,33 +787,11 @@ mod tests { } #[async_test] - async fn test_share_with_per_device_strategy_only_trusted() { - test_share_only_trusted_helper(false).await; - } - - /// Variation of [`test_share_with_per_device_strategy_only_trusted`] to - /// test the interaction between - /// [`only_allow_trusted_devices`](`CollectStrategy::DeviceBasedStrategy::only_allow_trusted_devices`) and - /// [`error_on_verified_user_problem`](`CollectStrategy::DeviceBasedStrategy::error_on_verified_user_problem`). - /// - /// (Given that untrusted devices are ignored, we do not expect - /// [`collect_session_recipients`] to return an error, despite the presence - /// of unsigned devices.) - #[async_test] - async fn test_share_with_per_device_strategy_only_trusted_error_on_unsigned_of_verified() { - test_share_only_trusted_helper(true).await; - } - - /// Common helper for [`test_share_with_per_device_strategy_only_trusted`] - /// and [`test_share_with_per_device_strategy_only_trusted_error_on_unsigned_of_verified`]. - async fn test_share_only_trusted_helper(error_on_verified_user_problem: bool) { + async fn test_share_with_only_trusted_strategy() { let machine = set_up_test_machine().await; let encryption_settings = EncryptionSettings { - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: true, - error_on_verified_user_problem, - }, + sharing_strategy: CollectStrategy::OnlyTrustedDevices, ..Default::default() }; @@ -1090,10 +1232,7 @@ mod tests { async fn test_share_with_identity_strategy() { let machine = set_up_test_machine().await; - let strategy = CollectStrategy::new_identity_based(); - - let encryption_settings = - EncryptionSettings { sharing_strategy: strategy.clone(), ..Default::default() }; + let encryption_settings = identity_based_strategy_settings(); let group_session = create_test_outbound_group_session(&machine, &encryption_settings); @@ -1169,10 +1308,7 @@ mod tests { let fake_room_id = room_id!("!roomid:localhost"); - let encryption_settings = EncryptionSettings { - sharing_strategy: CollectStrategy::new_identity_based(), - ..Default::default() - }; + let encryption_settings = identity_based_strategy_settings(); let request_result = machine .share_room_key( @@ -1295,10 +1431,7 @@ mod tests { let fake_room_id = room_id!("!roomid:localhost"); // We share the key using the identity-based strategy. - let encryption_settings = EncryptionSettings { - sharing_strategy: CollectStrategy::new_identity_based(), - ..Default::default() - }; + let encryption_settings = identity_based_strategy_settings(); let request_result = machine .share_room_key( @@ -1390,10 +1523,7 @@ mod tests { async fn test_should_rotate_based_on_visibility() { let machine = set_up_test_machine().await; - let strategy = CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: false, - }; + let strategy = CollectStrategy::AllDevices; let encryption_settings = EncryptionSettings { sharing_strategy: strategy.clone(), @@ -1439,14 +1569,7 @@ mod tests { let machine = set_up_test_machine().await; let fake_room_id = room_id!("!roomid:localhost"); - - let strategy = CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: false, - }; - - let encryption_settings = - EncryptionSettings { sharing_strategy: strategy.clone(), ..Default::default() }; + let encryption_settings = all_devices_strategy_settings(); let requests = machine .share_room_key( @@ -1538,13 +1661,24 @@ mod tests { machine } - /// [`EncryptionSettings`] with `error_on_verified_user_problem` set + /// [`EncryptionSettings`] with [`CollectStrategy::AllDevices`] + fn all_devices_strategy_settings() -> EncryptionSettings { + EncryptionSettings { sharing_strategy: CollectStrategy::AllDevices, ..Default::default() } + } + + /// [`EncryptionSettings`] with + /// [`CollectStrategy::ErrorOnVerifiedUserProblem`] fn error_on_verification_problem_encryption_settings() -> EncryptionSettings { EncryptionSettings { - sharing_strategy: CollectStrategy::DeviceBasedStrategy { - only_allow_trusted_devices: false, - error_on_verified_user_problem: true, - }, + sharing_strategy: CollectStrategy::ErrorOnVerifiedUserProblem, + ..Default::default() + } + } + + /// [`EncryptionSettings`] with [`CollectStrategy::IdentityBasedStrategy`] + fn identity_based_strategy_settings() -> EncryptionSettings { + EncryptionSettings { + sharing_strategy: CollectStrategy::IdentityBasedStrategy, ..Default::default() } } diff --git a/crates/matrix-sdk-crypto/src/session_manager/group_sessions/snapshots/matrix_sdk_crypto__session_manager__group_sessions__share_strategy__tests__serialize_device_based_strategy.snap b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/snapshots/matrix_sdk_crypto__session_manager__group_sessions__share_strategy__tests__serialize_device_based_strategy.snap new file mode 100644 index 000000000..add8ae3d0 --- /dev/null +++ b/crates/matrix-sdk-crypto/src/session_manager/group_sessions/snapshots/matrix_sdk_crypto__session_manager__group_sessions__share_strategy__tests__serialize_device_based_strategy.snap @@ -0,0 +1,5 @@ +--- +source: crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs +expression: serialized +--- +{"algorithm":"m.megolm.v1.aes-sha2","rotation_period":{"secs":604800,"nanos":0},"rotation_period_msgs":100,"history_visibility":"shared","sharing_strategy":"AllDevices"} diff --git a/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql b/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql index 1de6495cc..c3f8e07a0 100644 --- a/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql +++ b/crates/matrix-sdk-sqlite/migrations/event_cache_store/003_events.sql @@ -36,7 +36,7 @@ CREATE TABLE "events" ( -- `OwnedEventId` for events, can be null if malformed. "event_id" TEXT, - -- JSON serialized `SyncTimelineEvent` (encrypted value). + -- JSON serialized `TimelineEvent` (encrypted value). "content" BLOB NOT NULL, -- Position (index) in the chunk. "position" INTEGER NOT NULL, diff --git a/crates/matrix-sdk-ui/CHANGELOG.md b/crates/matrix-sdk-ui/CHANGELOG.md index 42e43a399..201b2a8c5 100644 --- a/crates/matrix-sdk-ui/CHANGELOG.md +++ b/crates/matrix-sdk-ui/CHANGELOG.md @@ -24,6 +24,18 @@ All notable changes to this project will be documented in this file. implement `Into` also implement `Into`. ([#4451](https://github.com/matrix-org/matrix-rust-sdk/pull/4451)) +### Refactor + +- [**breaking**] `Timeline::paginate_forwards` and `Timeline::paginate_backwards` + are unified to work on a live or focused timeline. + `Timeline::live_paginate_*` and `Timeline::focused_paginate_*` have been + removed ([#4584](https://github.com/matrix-org/matrix-rust-sdk/pull/4584)). +- [**breaking**] `Timeline::subscribe_batched` replaces + `Timeline::subscribe`. `subscribe` has been removed in + [#4567](https://github.com/matrix-org/matrix-rust-sdk/pull/4567), + and `subscribe_batched` has been renamed to `subscribe` in + [#4585](https://github.com/matrix-org/matrix-rust-sdk/pull/4585). + ## [0.9.0] - 2024-12-18 ### Bug Fixes @@ -63,7 +75,6 @@ All notable changes to this project will be documented in this file. - `EncryptionSyncService` and `Notification` are using `Client::cross_process_store_locks_holder_name`. - ### Refactor - [**breaking**] `Timeline::edit` now takes a `RoomMessageEventContentWithoutRelation`. diff --git a/crates/matrix-sdk-ui/Cargo.toml b/crates/matrix-sdk-ui/Cargo.toml index ea518a8f7..2b3e1eb88 100644 --- a/crates/matrix-sdk-ui/Cargo.toml +++ b/crates/matrix-sdk-ui/Cargo.toml @@ -51,6 +51,9 @@ tracing = { workspace = true, features = ["attributes"] } unicode-normalization = { workspace = true } uniffi = { workspace = true, optional = true } +emojis = "0.6.4" +unicode-segmentation = "1.12.0" + [dev-dependencies] anyhow = { workspace = true } assert-json-diff = { workspace = true } @@ -61,6 +64,7 @@ matrix-sdk = { workspace = true, features = ["testing", "sqlite"] } matrix-sdk-test = { workspace = true } stream_assert = { workspace = true } tempfile = { workspace = true } +url = { workspace = true } wiremock = { workspace = true } [lints] diff --git a/crates/matrix-sdk-ui/src/encryption_sync_service.rs b/crates/matrix-sdk-ui/src/encryption_sync_service.rs index 67148fc7f..fcbf4ef2f 100644 --- a/crates/matrix-sdk-ui/src/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/src/encryption_sync_service.rs @@ -31,7 +31,7 @@ use std::{pin::Pin, time::Duration}; use async_stream::stream; use futures_core::stream::Stream; use futures_util::{pin_mut, StreamExt}; -use matrix_sdk::{Client, SlidingSync, LEASE_DURATION_MS}; +use matrix_sdk::{sleep::sleep, Client, SlidingSync, LEASE_DURATION_MS}; use matrix_sdk_base::sliding_sync::http; use ruma::assign; use tokio::sync::OwnedMutexGuard; @@ -174,7 +174,7 @@ impl EncryptionSyncService { LEASE_DURATION_MS ); - tokio::time::sleep(Duration::from_millis(LEASE_DURATION_MS.into())).await; + sleep(Duration::from_millis(LEASE_DURATION_MS.into())).await; lock_guard = self .client diff --git a/crates/matrix-sdk-ui/src/notification_client.rs b/crates/matrix-sdk-ui/src/notification_client.rs index 5af760e71..07571f406 100644 --- a/crates/matrix-sdk-ui/src/notification_client.rs +++ b/crates/matrix-sdk-ui/src/notification_client.rs @@ -18,7 +18,9 @@ use std::{ }; use futures_util::{pin_mut, StreamExt as _}; -use matrix_sdk::{room::Room, Client, ClientBuildError, SlidingSyncList, SlidingSyncMode}; +use matrix_sdk::{ + room::Room, sleep::sleep, Client, ClientBuildError, SlidingSyncList, SlidingSyncMode, +}; use matrix_sdk_base::{ deserialized_responses::TimelineEvent, sliding_sync::http, RoomState, StoreError, }; @@ -212,7 +214,7 @@ impl NotificationClient { for _ in 0..3 { trace!("waiting for decryption…"); - tokio::time::sleep(Duration::from_millis(wait)).await; + sleep(Duration::from_millis(wait)).await; let new_event = room.decrypt_event(raw_event.cast_ref()).await?; diff --git a/crates/matrix-sdk-ui/src/room_list_service/mod.rs b/crates/matrix-sdk-ui/src/room_list_service/mod.rs index 37a36145c..a76643ad3 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/mod.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/mod.rs @@ -63,8 +63,8 @@ use async_stream::stream; use eyeball::Subscriber; use futures_util::{pin_mut, Stream, StreamExt}; use matrix_sdk::{ - event_cache::EventCacheError, Client, Error as SlidingSyncError, SlidingSync, SlidingSyncList, - SlidingSyncMode, + event_cache::EventCacheError, timeout::timeout, Client, Error as SlidingSyncError, SlidingSync, + SlidingSyncList, SlidingSyncMode, }; use matrix_sdk_base::sliding_sync::http; pub use room::*; @@ -72,7 +72,6 @@ pub use room_list::*; use ruma::{assign, directory::RoomTypeFilter, events::StateEventType, OwnedRoomId, RoomId, UInt}; pub use state::*; use thiserror::Error; -use tokio::time::timeout; use tracing::debug; use crate::timeline; @@ -328,7 +327,7 @@ impl RoomListService { }; // `state.next().await` has a maximum of `yield_delay` time to execute… - let next_state = match timeout(yield_delay, state.next()).await { + let next_state = match timeout(state.next(), yield_delay).await { // A new state has been received before `yield_delay` time. The new // `sync_indicator` value won't be yielded. Ok(next_state) => next_state, @@ -470,8 +469,8 @@ mod tests { use assert_matches::assert_matches; use futures_util::{pin_mut, StreamExt}; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, reqwest::Url, sliding_sync::Version as SlidingSyncVersion, Client, SlidingSyncMode, diff --git a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs index 99d8fa10b..0d79ac3ca 100644 --- a/crates/matrix-sdk-ui/src/room_list_service/room_list.rs +++ b/crates/matrix-sdk-ui/src/room_list_service/room_list.rs @@ -177,7 +177,7 @@ impl RoomList { Box::new(new_sorter_recency()), Box::new(new_sorter_name()) ])) - .dynamic_limit_with_initial_value(page_size, limit_stream.clone()); + .dynamic_head_with_initial_value(page_size, limit_stream.clone()); // Clearing the stream before chaining with the real stream. yield stream::once(ready(vec![VectorDiff::Reset { values }])) diff --git a/crates/matrix-sdk-ui/src/sync_service.rs b/crates/matrix-sdk-ui/src/sync_service.rs index 752d3eb6f..546031938 100644 --- a/crates/matrix-sdk-ui/src/sync_service.rs +++ b/crates/matrix-sdk-ui/src/sync_service.rs @@ -18,12 +18,11 @@ //! This is an opiniated way to run both APIs, with high-level callbacks that //! should be called in reaction to user actions and/or system events. //! -//! The sync service will signal errors via its -//! [`state`](SyncService::state) that the user -//! MUST observe. Whenever an error/termination is observed, the user MUST call -//! [`SyncService::start()`] again to restart the room list sync. +//! The sync service will signal errors via its [`state`](SyncService::state) +//! that the user MUST observe. Whenever an error/termination is observed, the +//! user MUST call [`SyncService::start()`] again to restart the room list sync. -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use eyeball::{SharedObservable, Subscriber}; use futures_core::Future; @@ -47,9 +46,9 @@ use crate::{ /// Current state of the application. /// /// This is a high-level state indicating what's the status of the underlying -/// syncs. The application starts in `Running` mode, and then hits a terminal -/// state `Terminated` (if it gracefully exited) or `Error` (in case any of the -/// underlying syncs ran into an error). +/// syncs. The application starts in [`State::Running`] mode, and then hits a +/// terminal state [`State::Terminated`] (if it gracefully exited) or +/// [`State::Error`] (in case any of the underlying syncs ran into an error). /// /// It is the responsibility of the caller to restart the application using the /// [`SyncService::start`] method, in case it terminated, gracefully or not. @@ -67,146 +66,144 @@ pub enum State { Error, } -pub struct SyncService { - /// Room list service used to synchronize the rooms state. - room_list_service: Arc, - - /// Encryption sync taking care of e2ee events. - encryption_sync_service: Arc, - - /// What's the state of this sync service? - state: SharedObservable, - - /// Use a mutex everytime to modify the `state` value, otherwise it would be - /// possible to have race conditions when starting or pausing the - /// service multiple times really quickly. - modifying_state: AsyncMutex<()>, - - /// Task running the room list service. - room_list_task: Arc>>>, - - /// Task running the encryption sync. - encryption_sync_task: Arc>>>, - - /// Global lock to allow using at most one `EncryptionSyncService` at all - /// times. - /// - /// This ensures that there's only one ever existing in the application's - /// lifetime (under the assumption that there is at most one - /// `SyncService` per application). - encryption_sync_permit: Arc>, - - /// Scheduler task ensuring proper termination. - /// - /// This task is waiting for a `TerminationReport` from any of the other two - /// tasks, or from a user request via [`Self::stop()`]. It makes sure - /// that the two services are properly shut up and just interrupted. - /// - /// This is set at the same time as the other two tasks. - scheduler_task: Arc>>>, - - /// `TerminationReport` sender for the [`Self::stop()`] function. - /// - /// This is set at the same time as all the tasks in [`Self::start()`]. - scheduler_sender: Mutex>>, +/// A supervisor responsible for managing two sync tasks: one for handling the +/// room list and another for supporting end-to-end encryption. +/// +/// The two sync tasks are spawned as child tasks and are contained within the +/// supervising task, which is stored in the [`SyncTaskSupervisor::task`] field. +/// +/// The supervisor ensures the two child tasks are managed as a single unit, +/// allowing for them to be shutdown in unison. +struct SyncTaskSupervisor { + /// The supervising task that manages and contains the two sync child tasks. + task: JoinHandle<()>, + /// [`TerminationReport`] sender for the [`SyncTaskSupervisor::shutdown()`] + /// function. + termination_sender: Sender, } -impl SyncService { - /// Create a new builder for configuring an `SyncService`. - pub fn builder(client: Client) -> SyncServiceBuilder { - SyncServiceBuilder::new(client) +impl SyncTaskSupervisor { + async fn new( + inner: &SyncServiceInner, + room_list_service: Arc, + encryption_sync_permit: Arc>, + ) -> Self { + let (sender, receiver) = tokio::sync::mpsc::channel(16); + let (room_list_task, encryption_sync_task) = Self::spawn_child_tasks( + inner, + room_list_service.clone(), + encryption_sync_permit, + sender.clone(), + ) + .await; + + let task = spawn(Self::spawn_supervisor_task( + inner, + room_list_service, + room_list_task, + encryption_sync_task, + receiver, + )); + + Self { task, termination_sender: sender } } - /// Get the underlying `RoomListService` instance for easier access to its - /// methods. - pub fn room_list_service(&self) -> Arc { - self.room_list_service.clone() - } - - /// Returns the state of the sync service. - pub fn state(&self) -> Subscriber { - self.state.subscribe() - } - - /// The role of the scheduler task is to wait for a termination message - /// (`TerminationReport`), sent either because we wanted to stop both - /// syncs, or because one of the syncs failed (in which case we'll stop - /// the other one too). - fn spawn_scheduler_task( - &self, + /// The role of the supervisor task is to wait for a termination message + /// ([`TerminationReport`]), sent either because we wanted to stop both + /// syncs, or because one of the syncs failed (in which case we'll stop the + /// other one too). + fn spawn_supervisor_task( + inner: &SyncServiceInner, + room_list_service: Arc, + room_list_task: JoinHandle<()>, + encryption_sync_task: JoinHandle<()>, mut receiver: Receiver, ) -> impl Future { - let encryption_sync_task = self.encryption_sync_task.clone(); - let encryption_sync = self.encryption_sync_service.clone(); - let room_list_service = self.room_list_service.clone(); - let room_list_task = self.room_list_task.clone(); - let state = self.state.clone(); + let encryption_sync = inner.encryption_sync_service.clone(); + let state = inner.state.clone(); async move { - let Some(report) = receiver.recv().await else { + let report = if let Some(report) = receiver.recv().await { + report + } else { info!("internal channel has been closed?"); - return; + // We should still stop the child tasks in the unlikely scenario that our + // receiver died. + TerminationReport::supervisor_error() }; // If one service failed, make sure to request stopping the other one. let (stop_room_list, stop_encryption) = match &report.origin { TerminationOrigin::EncryptionSync => (true, false), TerminationOrigin::RoomList => (false, true), - TerminationOrigin::Scheduler => (true, true), + TerminationOrigin::Supervisor => (true, true), }; // Stop both services, and wait for the streams to properly finish: at some - // point they'll return `None` and will exit their infinite loops, - // and their tasks will gracefully terminate. + // point they'll return `None` and will exit their infinite loops, and their + // tasks will gracefully terminate. if stop_room_list { if let Err(err) = room_list_service.stop_sync() { warn!(?report, "unable to stop room list service: {err:#}"); } + + if report.has_expired { + room_list_service.expire_sync_session().await; + } } - { - let task = room_list_task.lock().unwrap().take(); - if let Some(task) = task { - if let Err(err) = task.await { - error!("when awaiting room list service: {err:#}"); - } - } + if let Err(err) = room_list_task.await { + error!("when awaiting room list service: {err:#}"); } if stop_encryption { if let Err(err) = encryption_sync.stop_sync() { warn!(?report, "unable to stop encryption sync: {err:#}"); } + + if report.has_expired { + encryption_sync.expire_sync_session().await; + } } - { - let task = encryption_sync_task.lock().unwrap().take(); - if let Some(task) = task { - if let Err(err) = task.await { - error!("when awaiting encryption sync: {err:#}"); - } - } + if let Err(err) = encryption_sync_task.await { + error!("when awaiting encryption sync: {err:#}"); } if report.is_error { - if report.has_expired { - if stop_room_list { - room_list_service.expire_sync_session().await; - } - if stop_encryption { - encryption_sync.expire_sync_session().await; - } - } - state.set(State::Error); - } else if matches!(report.origin, TerminationOrigin::Scheduler) { + } else if matches!(report.origin, TerminationOrigin::Supervisor) { state.set(State::Idle); } else { state.set(State::Terminated); } } - .instrument(tracing::span!(Level::WARN, "scheduler task")) + .instrument(tracing::span!(Level::WARN, "supervisor task")) + } + + async fn spawn_child_tasks( + inner: &SyncServiceInner, + room_list_service: Arc, + encryption_sync_permit: Arc>, + sender: Sender, + ) -> (JoinHandle<()>, JoinHandle<()>) { + // First, take care of the room list. + let room_list_task = spawn(Self::room_list_sync_task(room_list_service, sender.clone())); + + // Then, take care of the encryption sync. + let sync_permit_guard = encryption_sync_permit.clone().lock_owned().await; + let encryption_sync_task = spawn(Self::encryption_sync_task( + inner.encryption_sync_service.clone(), + sender.clone(), + sync_permit_guard, + )); + + (room_list_task, encryption_sync_task) + } + + fn check_if_expired(err: &matrix_sdk::Error) -> bool { + err.client_api_error_kind() == Some(&ruma::api::client::error::ErrorKind::UnknownPos) } async fn encryption_sync_task( @@ -214,28 +211,29 @@ impl SyncService { sender: Sender, sync_permit_guard: OwnedMutexGuard, ) { + use encryption_sync_service::Error; + let encryption_sync_stream = encryption_sync.sync(sync_permit_guard); pin_mut!(encryption_sync_stream); let (is_error, has_expired) = loop { - let res = encryption_sync_stream.next().await; - match res { + match encryption_sync_stream.next().await { Some(Ok(())) => { // Carry on. } Some(Err(err)) => { // If the encryption sync error was an expired session, also expire the // room list sync. - let has_expired = if let encryption_sync_service::Error::SlidingSync(err) = &err - { - err.client_api_error_kind() - == Some(&ruma::api::client::error::ErrorKind::UnknownPos) + let has_expired = if let Error::SlidingSync(err) = &err { + Self::check_if_expired(err) } else { false }; + if !has_expired { error!("Error while processing encryption in sync service: {err:#}"); } + break (true, has_expired); } None => { @@ -261,27 +259,29 @@ impl SyncService { room_list_service: Arc, sender: Sender, ) { + use room_list_service::Error; + let room_list_stream = room_list_service.sync(); pin_mut!(room_list_stream); let (is_error, has_expired) = loop { - let res = room_list_stream.next().await; - match res { + match room_list_stream.next().await { Some(Ok(())) => { // Carry on. } Some(Err(err)) => { // If the room list error was an expired session, also expire the // encryption sync. - let has_expired = if let room_list_service::Error::SlidingSync(err) = &err { - err.client_api_error_kind() - == Some(&ruma::api::client::error::ErrorKind::UnknownPos) + let has_expired = if let Error::SlidingSync(err) = &err { + Self::check_if_expired(err) } else { false }; + if !has_expired { error!("Error while processing room list in sync service: {err:#}"); } + break (true, has_expired); } None => { @@ -299,6 +299,129 @@ impl SyncService { } } + async fn shutdown(self) -> Result<(), Error> { + match self + .termination_sender + .send(TerminationReport { + is_error: false, + has_expired: false, + origin: TerminationOrigin::Supervisor, + }) + .await + { + Ok(_) => self.task.await.map_err(|err| { + error!("couldn't finish supervisor task: {err}"); + Error::InternalSupervisorError + }), + Err(err) => { + error!("when sending termination report: {err}"); + // Let's abort the task if it won't shut down properly, otherwise we would have + // left it as a detached task. + self.task.abort(); + Err(Error::InternalSupervisorError) + } + } + } +} + +struct SyncServiceInner { + encryption_sync_service: Arc, + state: SharedObservable, + /// Supervisor task ensuring proper termination. + /// + /// This task is waiting for a [`TerminationReport`] from any of the other + /// two tasks, or from a user request via [`SyncService::stop()`]. It + /// makes sure that the two services are properly shut up and just + /// interrupted. + /// + /// This is set at the same time as the other two tasks. + supervisor: Option, +} + +/// A high level manager for your Matrix syncing needs. +/// +/// The [`SyncService`] is responsible for managing real-time synchronization +/// with a Matrix server. It can initiate and maintain the necessary +/// synchronization tasks for you. +/// +/// **Note**: The [`SyncService`] requires a server with support for [MSC4186], +/// otherwise it will fail with an 404 `M_UNRECOGNIZED` request error. +/// +/// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186/ +/// +/// # Example +/// +/// ```no_run +/// use matrix_sdk::Client; +/// use matrix_sdk_ui::sync_service::{State, SyncService}; +/// # use url::Url; +/// # async { +/// let homeserver = Url::parse("http://example.com")?; +/// let client = Client::new(homeserver).await?; +/// +/// client +/// .matrix_auth() +/// .login_username("example", "wordpass") +/// .initial_device_display_name("My bot") +/// .await?; +/// +/// let sync_service = SyncService::builder(client).build().await?; +/// let mut state = sync_service.state(); +/// +/// while let Some(state) = state.next().await { +/// match state { +/// State::Idle => eprintln!("The sync service is idle."), +/// State::Running => eprintln!("The sync has started to run."), +/// State::Terminated => { +/// eprintln!("The sync service has been gracefully terminated"); +/// break; +/// } +/// State::Error => { +/// eprintln!("The sync service has run into an error"); +/// break; +/// } +/// } +/// } +/// # anyhow::Ok(()) }; +/// ``` +pub struct SyncService { + inner: Arc>, + + /// Room list service used to synchronize the rooms state. + room_list_service: Arc, + + /// What's the state of this sync service? This field is replicated from the + /// [`SyncServiceInner`] struct, but it should not be modified in this + /// struct. It's re-exposed here so we can subscribe to the state without + /// taking the lock on the `inner` field. + state: SharedObservable, + + /// Global lock to allow using at most one [`EncryptionSyncService`] at all + /// times. + /// + /// This ensures that there's only one ever existing in the application's + /// lifetime (under the assumption that there is at most one [`SyncService`] + /// per application). + encryption_sync_permit: Arc>, +} + +impl SyncService { + /// Create a new builder for configuring an `SyncService`. + pub fn builder(client: Client) -> SyncServiceBuilder { + SyncServiceBuilder::new(client) + } + + /// Get the underlying `RoomListService` instance for easier access to its + /// methods. + pub fn room_list_service(&self) -> Arc { + self.room_list_service.clone() + } + + /// Returns the state of the sync service. + pub fn state(&self) -> Subscriber { + self.state.subscribe() + } + /// Start (or restart) the underlying sliding syncs. /// /// This can be called multiple times safely: @@ -306,35 +429,26 @@ impl SyncService { /// - if the stream has been aborted before, it will be properly cleaned up /// and restarted. pub async fn start(&self) { - let _guard = self.modifying_state.lock().await; + let mut inner = self.inner.lock().await; // Only (re)start the tasks if any was stopped. - if matches!(self.state.get(), State::Running) { - // It was already true, so we can skip the restart. - return; + match inner.state.get() { + // If we're already running, there's nothing to do. + State::Running => (), + State::Idle | State::Terminated | State::Error => { + trace!("starting sync service"); + + inner.supervisor = Some( + SyncTaskSupervisor::new( + &inner, + self.room_list_service.clone(), + self.encryption_sync_permit.clone(), + ) + .await, + ); + inner.state.set(State::Running); + } } - - trace!("starting sync service"); - - let (sender, receiver) = tokio::sync::mpsc::channel(16); - - // First, take care of the room list. - *self.room_list_task.lock().unwrap() = - Some(spawn(Self::room_list_sync_task(self.room_list_service.clone(), sender.clone()))); - - // Then, take care of the encryption sync. - let sync_permit_guard = self.encryption_sync_permit.clone().lock_owned().await; - *self.encryption_sync_task.lock().unwrap() = Some(spawn(Self::encryption_sync_task( - self.encryption_sync_service.clone(), - sender.clone(), - sync_permit_guard, - ))); - - // Spawn the scheduler task. - *self.scheduler_sender.lock().unwrap() = Some(sender); - *self.scheduler_task.lock().unwrap() = Some(spawn(self.spawn_scheduler_task(receiver))); - - self.state.set(State::Running); } /// Stop the underlying sliding syncs. @@ -344,52 +458,29 @@ impl SyncService { /// necessary. #[instrument(skip_all)] pub async fn stop(&self) -> Result<(), Error> { - let _guard = self.modifying_state.lock().await; + let mut inner = self.inner.lock().await; - match self.state.get() { + match inner.state.get() { State::Idle | State::Terminated | State::Error => { // No need to stop if we were not running. return Ok(()); } - State::Running => {} - }; + State::Running => (), + } trace!("pausing sync service"); // First, request to stop the two underlying syncs; we'll look at the results - // later, so that we're in a clean state independently of the request to - // stop. + // later, so that we're in a clean state independently of the request to stop. - let sender = self.scheduler_sender.lock().unwrap().clone(); - sender - .ok_or_else(|| { - error!("missing sender"); - Error::InternalSchedulerError - })? - .send(TerminationReport { - is_error: false, - has_expired: false, - origin: TerminationOrigin::Scheduler, - }) - .await - .map_err(|err| { - error!("when sending termination report: {err}"); - Error::InternalSchedulerError - })?; + // Remove the supervisor from our inner state and request the tasks to be + // shutdown. + let supervisor = inner.supervisor.take().ok_or_else(|| { + error!("The supervisor was not properly started up"); + Error::InternalSupervisorError + })?; - let scheduler_task = self.scheduler_task.lock().unwrap().take(); - scheduler_task - .ok_or_else(|| { - error!("missing scheduler task"); - Error::InternalSchedulerError - })? - .await - .map_err(|err| { - error!("couldn't finish scheduler task: {err}"); - Error::InternalSchedulerError - })?; - - Ok(()) + supervisor.shutdown().await } /// Attempt to get a permit to use an `EncryptionSyncService` at a given @@ -406,7 +497,7 @@ impl SyncService { enum TerminationOrigin { EncryptionSync, RoomList, - Scheduler, + Supervisor, } #[derive(Debug)] @@ -416,15 +507,22 @@ struct TerminationReport { origin: TerminationOrigin, } +impl TerminationReport { + fn supervisor_error() -> Self { + TerminationReport { + is_error: true, + has_expired: false, + origin: TerminationOrigin::Supervisor, + } + } +} + // Testing helpers, mostly. #[doc(hidden)] impl SyncService { - /// Return the existential states of internal tasks. - pub fn task_states(&self) -> (bool, bool) { - ( - self.encryption_sync_task.lock().unwrap().is_some(), - self.room_list_task.lock().unwrap().is_some(), - ) + /// Is the task supervisor running? + pub async fn is_supervisor_running(&self) -> bool { + self.inner.lock().await.supervisor.is_some() } } @@ -457,11 +555,11 @@ impl SyncServiceBuilder { self } - /// Finish setting up the `SyncService`. + /// Finish setting up the [`SyncService`]. /// /// This creates the underlying sliding syncs, and will *not* start them in - /// the background. The resulting `SyncService` must be kept alive as - /// long as the sliding syncs are supposed to run. + /// the background. The resulting [`SyncService`] must be kept alive as long + /// as the sliding syncs are supposed to run. pub async fn build(self) -> Result { let encryption_sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new())); @@ -476,16 +574,18 @@ impl SyncServiceBuilder { .await?, ); + let room_list_service = Arc::new(room_list); + let state = SharedObservable::new(State::Idle); + Ok(SyncService { - room_list_service: Arc::new(room_list), - encryption_sync_service: encryption_sync, - encryption_sync_task: Arc::new(Mutex::new(None)), - room_list_task: Arc::new(Mutex::new(None)), - scheduler_task: Arc::new(Mutex::new(None)), - scheduler_sender: Mutex::new(None), - state: SharedObservable::new(State::Idle), - modifying_state: AsyncMutex::new(()), + state: state.clone(), + room_list_service, encryption_sync_permit, + inner: Arc::new(AsyncMutex::new(SyncServiceInner { + supervisor: None, + encryption_sync_service: encryption_sync, + state, + })), }) } } @@ -501,6 +601,7 @@ pub enum Error { #[error(transparent)] EncryptionSync(#[from] encryption_sync_service::Error), - #[error("the scheduler channel has run into an unexpected error")] - InternalSchedulerError, + /// An error had occurred in the sync task supervisor, likely due to a bug. + #[error("the supervisor channel has run into an unexpected error")] + InternalSupervisorError, } diff --git a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs index 64048aeda..c370f830d 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/mod.rs @@ -20,14 +20,14 @@ use eyeball_im_util::vector::VectorObserverExt; use futures_core::Stream; use imbl::Vector; #[cfg(test)] -use matrix_sdk::crypto::OlmMachine; +use matrix_sdk::{crypto::OlmMachine, SendOutsideWasm}; use matrix_sdk::{ - deserialized_responses::{SyncTimelineEvent, TimelineEventKind as SdkTimelineEventKind}, + deserialized_responses::{TimelineEvent, TimelineEventKind as SdkTimelineEventKind}, event_cache::{paginator::Paginator, RoomEventCache}, send_queue::{ LocalEcho, LocalEchoContent, RoomSendQueueUpdate, SendHandle, SendReactionHandle, }, - Result, Room, SendOutsideWasm, + Result, Room, }; use ruma::{ api::client::receipt::create_receipt::v3::ReceiptType as SendReceiptType, @@ -396,7 +396,7 @@ impl TimelineController

{ pub(crate) async fn reload_pinned_events( &self, - ) -> Result, PinnedEventsLoaderError> { + ) -> Result, PinnedEventsLoaderError> { let focus_guard = self.focus.read().await; if let TimelineFocusData::PinnedEvents { loader } = &*focus_guard { @@ -477,13 +477,13 @@ impl TimelineController

{ self.state.read().await.items.clone_items() } + #[cfg(test)] pub(super) async fn subscribe( &self, ) -> ( Vector>, impl Stream>> + SendOutsideWasm, ) { - trace!("Creating timeline items signal"); let state = self.state.read().await; (state.items.clone_items(), state.items.subscribe().into_stream()) } @@ -491,7 +491,6 @@ impl TimelineController

{ pub(super) async fn subscribe_batched( &self, ) -> (Vector>, impl Stream>>>) { - trace!("Creating timeline items signal"); let state = self.state.read().await; (state.items.clone_items(), state.items.subscribe().into_batched_stream()) } @@ -504,7 +503,6 @@ impl TimelineController

{ U: Clone, F: Fn(Arc) -> Option, { - trace!("Creating timeline items signal"); self.state.read().await.items.subscribe().filter_map(f) } @@ -662,7 +660,7 @@ impl TimelineController

{ ) -> HandleManyEventsResult where Events: IntoIterator + ExactSizeIterator, - ::Item: Into, + ::Item: Into, { if events.len() == 0 { return Default::default(); @@ -675,7 +673,7 @@ impl TimelineController

{ /// Handle updates on events as [`VectorDiff`]s. pub(super) async fn handle_remote_events_with_diffs( &self, - diffs: Vec>, + diffs: Vec>, origin: RemoteEventOrigin, ) { if diffs.is_empty() { @@ -710,7 +708,7 @@ impl TimelineController

{ origin: RemoteEventOrigin, ) where Events: IntoIterator + ExactSizeIterator, - ::Item: Into, + ::Item: Into, { let mut state = self.state.write().await; diff --git a/crates/matrix-sdk-ui/src/timeline/controller/state.rs b/crates/matrix-sdk-ui/src/timeline/controller/state.rs index 7c6b3e527..75bd8512f 100644 --- a/crates/matrix-sdk-ui/src/timeline/controller/state.rs +++ b/crates/matrix-sdk-ui/src/timeline/controller/state.rs @@ -22,9 +22,8 @@ use std::{ use eyeball_im::VectorDiff; use itertools::Itertools as _; use matrix_sdk::{ - deserialized_responses::SyncTimelineEvent, ring_buffer::RingBuffer, send_queue::SendHandle, + deserialized_responses::TimelineEvent, ring_buffer::RingBuffer, send_queue::SendHandle, }; -use matrix_sdk_base::deserialized_responses::TimelineEvent; #[cfg(test)] use ruma::events::receipt::ReceiptEventContent; use ruma::{ @@ -139,7 +138,7 @@ impl TimelineState { ) -> HandleManyEventsResult where Events: IntoIterator + ExactSizeIterator, - ::Item: Into, + ::Item: Into, RoomData: RoomDataProvider, { if events.len() == 0 { @@ -157,7 +156,7 @@ impl TimelineState { /// Handle updates on events as [`VectorDiff`]s. pub(super) async fn handle_remote_events_with_diffs( &mut self, - diffs: Vec>, + diffs: Vec>, origin: RemoteEventOrigin, room_data: &RoomData, settings: &TimelineSettings, @@ -280,7 +279,7 @@ impl TimelineState { let handle_one_res = txn .handle_remote_event( - event.into(), + event, TimelineItemPosition::UpdateAt { timeline_item_index: idx }, room_data_provider, settings, @@ -331,7 +330,7 @@ impl TimelineState { ) -> HandleManyEventsResult where Events: IntoIterator, - Events::Item: Into, + Events::Item: Into, RoomData: RoomDataProvider, { let mut txn = self.transaction(); @@ -397,7 +396,7 @@ impl TimelineStateTransaction<'_> { ) -> HandleManyEventsResult where Events: IntoIterator, - Events::Item: Into, + Events::Item: Into, RoomData: RoomDataProvider, { let mut total = HandleManyEventsResult::default(); @@ -439,7 +438,7 @@ impl TimelineStateTransaction<'_> { /// Handle updates on events as [`VectorDiff`]s. pub(super) async fn handle_remote_events_with_diffs( &mut self, - diffs: Vec>, + diffs: Vec>, origin: RemoteEventOrigin, room_data_provider: &RoomData, settings: &TimelineSettings, @@ -559,13 +558,13 @@ impl TimelineStateTransaction<'_> { /// Returns the number of timeline updates that were made. async fn handle_remote_event( &mut self, - event: SyncTimelineEvent, + event: TimelineEvent, position: TimelineItemPosition, room_data_provider: &P, settings: &TimelineSettings, date_divider_adjuster: &mut DateDividerAdjuster, ) -> HandleEventResult { - let SyncTimelineEvent { push_actions, kind } = event; + let TimelineEvent { push_actions, kind } = event; let encryption_info = kind.encryption_info().cloned(); let (raw, utd_info) = match kind { @@ -758,7 +757,9 @@ impl TimelineStateTransaction<'_> { } else { Default::default() }, - is_highlighted: push_actions.iter().any(Action::is_highlight), + is_highlighted: push_actions + .as_ref() + .is_some_and(|actions| actions.iter().any(Action::is_highlight)), flow: Flow::Remote { event_id: event_id.clone(), raw_event: raw, diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index 5a3d362ea..6759a2e52 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -211,7 +211,7 @@ impl TimelineEventKind { Self::UnableToDecrypt { content, utd_cause } } else { // If we get here, it means that some part of the code has created a - // `SyncTimelineEvent` containing an `m.room.encrypted` event + // `TimelineEvent` containing an `m.room.encrypted` event // without decrypting it. Possibly this means that encryption has not been // configured. // We treat it the same as any other message-like event. diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index 9873c0a09..3365d5298 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -36,6 +36,7 @@ use ruma::{ OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId, }; use tracing::warn; +use unicode_segmentation::UnicodeSegmentation; mod content; mod local; @@ -127,14 +128,17 @@ impl EventTimelineItem { Self { sender, sender_profile, timestamp, content, reactions, kind, is_room_encrypted } } - /// If the supplied low-level `SyncTimelineEvent` is suitable for use as the - /// `latest_event` in a message preview, wrap it as an `EventTimelineItem`. + /// If the supplied low-level [`TimelineEvent`] is suitable for use as the + /// `latest_event` in a message preview, wrap it as an + /// `EventTimelineItem`. /// /// **Note:** Timeline items created via this constructor do **not** produce /// the correct ShieldState when calling /// [`get_shield`][EventTimelineItem::get_shield]. This is because they are /// intended for display in the room list which a) is unlikely to show /// shields and b) would incur a significant performance overhead. + /// + /// [`TimelineEvent`]: matrix_sdk::deserialized_responses::TimelineEvent pub async fn from_latest_event( client: Client, room_id: &RoomId, @@ -601,6 +605,69 @@ impl EventTimelineItem { pub fn local_echo_send_handle(&self) -> Option { as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone()) } + + /// Some clients may want to know if a particular text message or media + /// caption contains only emojis so that they can render them bigger for + /// added effect. + /// + /// This function provides that feature with the following + /// behavior/limitations: + /// - ignores leading and trailing white spaces + /// - fails texts bigger than 5 graphemes for performance reasons + /// - checks the body only for [`MessageType::Text`] + /// - only checks the caption for [`MessageType::Audio`], + /// [`MessageType::File`], [`MessageType::Image`], and + /// [`MessageType::Video`] if present + /// - all other message types will not match + /// + /// # Examples + /// # fn render_timeline_item(timeline_item: TimelineItem) { + /// if timeline_item.contains_only_emojis() { + /// // e.g. increase the font size + /// } + /// # } + /// + /// See `test_emoji_detection` for more examples. + pub fn contains_only_emojis(&self) -> bool { + let body = match self.content() { + TimelineItemContent::Message(msg) => match msg.msgtype() { + MessageType::Text(text) => Some(text.body.as_str()), + MessageType::Audio(audio) => audio.caption(), + MessageType::File(file) => file.caption(), + MessageType::Image(image) => image.caption(), + MessageType::Video(video) => video.caption(), + _ => None, + }, + TimelineItemContent::RedactedMessage + | TimelineItemContent::Sticker(_) + | TimelineItemContent::UnableToDecrypt(_) + | TimelineItemContent::MembershipChange(_) + | TimelineItemContent::ProfileChange(_) + | TimelineItemContent::OtherState(_) + | TimelineItemContent::FailedToParseMessageLike { .. } + | TimelineItemContent::FailedToParseState { .. } + | TimelineItemContent::Poll(_) + | TimelineItemContent::CallInvite + | TimelineItemContent::CallNotify => None, + }; + + if let Some(body) = body { + // Collect the graphemes after trimming white spaces. + let graphemes = body.trim().graphemes(true).collect::>(); + + // Limit the check to 5 graphemes for performance and security + // reasons. This will probably be used for every new message so we + // want it to be fast and we don't want to allow a DoS attack by + // sending a huge message. + if graphemes.len() > 5 { + return false; + } + + graphemes.iter().all(|g| emojis::get(g).is_some()) + } else { + false + } + } } impl From for EventTimelineItemKind { @@ -754,7 +821,7 @@ mod tests { use assert_matches2::assert_let; use matrix_sdk::test_utils::logged_in_client; use matrix_sdk_base::{ - deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http, + deserialized_responses::TimelineEvent, latest_event::LatestEvent, sliding_sync::http, MinimalStateEvent, OriginalMinimalStateEvent, }; use matrix_sdk_test::{ @@ -844,7 +911,7 @@ mod tests { client.process_sliding_sync_test_helper(&response).await.unwrap(); // When we construct a timeline event from it - let event = SyncTimelineEvent::new(raw_event.cast()); + let event = TimelineEvent::new(raw_event.cast()); let timeline_item = EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event)) .await @@ -891,7 +958,7 @@ mod tests { .event_id(original_event_id) .bundled_relations(relations) .server_ts(42) - .into_sync(); + .into_event(); let client = logged_in_client(None).await; @@ -947,7 +1014,7 @@ mod tests { .event_id(original_event_id) .bundled_relations(relations) .sender(user_id) - .into_sync(); + .into_event(); let client = logged_in_client(None).await; @@ -1057,6 +1124,48 @@ mod tests { ); } + #[async_test] + async fn test_emoji_detection() { + let room_id = room_id!("!q:x.uk"); + let user_id = user_id!("@t:o.uk"); + let client = logged_in_client(None).await; + + let mut event = message_event(room_id, user_id, "🤷‍♂️ No boost 🤷‍♂️", "", 0); + let mut timeline_item = + EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) + .await + .unwrap(); + + assert!(!timeline_item.contains_only_emojis()); + + // Ignores leading and trailing white spaces + event = message_event(room_id, user_id, " 🚀 ", "", 0); + timeline_item = + EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) + .await + .unwrap(); + + assert!(timeline_item.contains_only_emojis()); + + // Too many + event = message_event(room_id, user_id, "👨‍👩‍👦1️⃣🚀👳🏾‍♂️🪩👍👍🏻🫱🏼‍🫲🏾🙂👋", "", 0); + timeline_item = + EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) + .await + .unwrap(); + + assert!(!timeline_item.contains_only_emojis()); + + // Works with combined emojis + event = message_event(room_id, user_id, "👨‍👩‍👦1️⃣👳🏾‍♂️👍🏻🫱🏼‍🫲🏾", "", 0); + timeline_item = + EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event)) + .await + .unwrap(); + + assert!(timeline_item.contains_only_emojis()); + } + fn member_event( room_id: &RoomId, user_id: &UserId, @@ -1121,8 +1230,8 @@ mod tests { body: &str, formatted_body: &str, ts: u64, - ) -> SyncTimelineEvent { - SyncTimelineEvent::new(sync_timeline_event!({ + ) -> TimelineEvent { + TimelineEvent::new(sync_timeline_event!({ "event_id": "$eventid6", "sender": user_id, "origin_server_ts": ts, diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 721992527..3b419f9e4 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -266,25 +266,14 @@ impl Timeline { } } - /// Get the current timeline items, and a stream of changes. + /// Get the current timeline items, along with a stream of updates of + /// timeline items. /// - /// You can poll this stream to receive updates. See - /// [`futures_util::StreamExt`] for a high-level API on top of [`Stream`]. + /// The stream produces `Vec>`, which means multiple updates + /// at once. There are no delays, it consumes as many updates as possible + /// and batches them. pub async fn subscribe( &self, - ) -> (Vector>, impl Stream>>) { - let (items, stream) = self.controller.subscribe().await; - let stream = TimelineStream::new(stream, self.drop_handle.clone()); - (items, stream) - } - - /// Get the current timeline items, and a batched stream of changes. - /// - /// In contrast to [`subscribe`](Self::subscribe), this stream can yield - /// multiple diffs at once. The batching is done such that no arbitrary - /// delays are added. - pub async fn subscribe_batched( - &self, ) -> (Vector>, impl Stream>>>) { let (items, stream) = self.controller.subscribe_batched().await; let stream = TimelineStream::new(stream, self.drop_handle.clone()); diff --git a/crates/matrix-sdk-ui/src/timeline/pagination.rs b/crates/matrix-sdk-ui/src/timeline/pagination.rs index e7322b215..4c79f5a4b 100644 --- a/crates/matrix-sdk-ui/src/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/src/timeline/pagination.rs @@ -36,26 +36,20 @@ impl super::Timeline { if self.controller.is_live().await { Ok(self.live_paginate_backwards(num_events).await?) } else { - Ok(self.focused_paginate_backwards(num_events).await?) + Ok(self.controller.focused_paginate_backwards(num_events).await?) } } - /// Assuming the timeline is focused on an event, starts a forwards - /// pagination. + /// Add more events to the end of the timeline. /// /// Returns whether we hit the end of the timeline. - #[instrument(skip_all)] - pub async fn focused_paginate_forwards(&self, num_events: u16) -> Result { - Ok(self.controller.focused_paginate_forwards(num_events).await?) - } - - /// Assuming the timeline is focused on an event, starts a backwards - /// pagination. - /// - /// Returns whether we hit the start of the timeline. - #[instrument(skip(self), fields(room_id = ?self.room().room_id()))] - pub async fn focused_paginate_backwards(&self, num_events: u16) -> Result { - Ok(self.controller.focused_paginate_backwards(num_events).await?) + #[instrument(skip_all, fields(room_id = ?self.room().room_id()))] + pub async fn paginate_forwards(&self, num_events: u16) -> Result { + if self.controller.is_live().await { + Ok(true) + } else { + Ok(self.controller.focused_paginate_forwards(num_events).await?) + } } /// Paginate backwards in live mode. @@ -64,8 +58,7 @@ impl super::Timeline { /// on a specific event. /// /// Returns whether we hit the start of the timeline. - #[instrument(skip_all, fields(room_id = ?self.room().room_id()))] - pub async fn live_paginate_backwards(&self, batch_size: u16) -> event_cache::Result { + async fn live_paginate_backwards(&self, batch_size: u16) -> event_cache::Result { let pagination = self.event_cache.pagination(); let result = pagination diff --git a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs index 2fa07fd67..28a9ed9a7 100644 --- a/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs +++ b/crates/matrix-sdk-ui/src/timeline/pinned_events_loader.rs @@ -19,7 +19,7 @@ use matrix_sdk::{ config::RequestConfig, event_cache::paginator::PaginatorError, BoxFuture, Room, SendOutsideWasm, SyncOutsideWasm, }; -use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_base::deserialized_responses::TimelineEvent; use ruma::{events::relation::RelationType, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId}; use thiserror::Error; use tracing::{debug, warn}; @@ -55,9 +55,9 @@ impl PinnedEventsLoader { /// `max_concurrent_requests` allows, to avoid overwhelming the server. /// /// It returns a `Result` with either a - /// chronologically sorted list of retrieved `SyncTimelineEvent`s - /// or a `PinnedEventsLoaderError`. - pub async fn load_events(&self) -> Result, PinnedEventsLoaderError> { + /// chronologically sorted list of retrieved [`TimelineEvent`]s + /// or a [`PinnedEventsLoaderError`]. + pub async fn load_events(&self) -> Result, PinnedEventsLoaderError> { let pinned_event_ids: Vec = self .room .pinned_event_ids() @@ -74,7 +74,7 @@ impl PinnedEventsLoader { let request_config = Some(RequestConfig::default().retry_limit(3)); - let mut loaded_events: Vec = + let mut loaded_events: Vec = stream::iter(pinned_event_ids.into_iter().map(|event_id| { let provider = self.room.clone(); let relations_filter = @@ -132,7 +132,7 @@ pub trait PinnedEventsRoom: SendOutsideWasm + SyncOutsideWasm { event_id: &'a EventId, request_config: Option, related_event_filters: Option>, - ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>>; + ) -> BoxFuture<'a, Result<(TimelineEvent, Vec), PaginatorError>>; /// Get the pinned event ids for a room. fn pinned_event_ids(&self) -> Option>; @@ -150,7 +150,7 @@ impl PinnedEventsRoom for Room { event_id: &'a EventId, request_config: Option, related_event_filters: Option>, - ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>> { + ) -> BoxFuture<'a, Result<(TimelineEvent, Vec), PaginatorError>> { Box::pin(async move { if let Ok((cache, _handles)) = self.event_cache().await { if let Some(ret) = cache.event_with_relations(event_id, related_event_filters).await @@ -163,7 +163,7 @@ impl PinnedEventsRoom for Room { debug!("Loading pinned event {event_id} from HS"); self.event(event_id, request_config) .await - .map(|e| (e.into(), Vec::new())) + .map(|e| (e, Vec::new())) .map_err(|err| PaginatorError::SdkError(Box::new(err))) }) } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs index a93502832..523835056 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/basic.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/basic.rs @@ -16,7 +16,7 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; -use matrix_sdk::deserialized_responses::SyncTimelineEvent; +use matrix_sdk::deserialized_responses::TimelineEvent; use matrix_sdk_test::{ async_test, event_factory::PreviousMembership, sync_timeline_event, ALICE, BOB, CAROL, }; @@ -89,7 +89,7 @@ async fn test_replace_with_initial_events_and_read_marker() { .with_settings(TimelineSettings { track_read_receipts: true, ..Default::default() }); let f = &timeline.factory; - let ev = f.text_msg("hey").sender(*ALICE).into_sync(); + let ev = f.text_msg("hey").sender(*ALICE).into_event(); timeline .controller @@ -104,7 +104,7 @@ async fn test_replace_with_initial_events_and_read_marker() { assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); - let ev = f.text_msg("yo").sender(*BOB).into_sync(); + let ev = f.text_msg("yo").sender(*BOB).into_event(); timeline .controller .replace_with_initial_remote_events([ev].into_iter(), RemoteEventOrigin::Sync) @@ -122,7 +122,7 @@ async fn test_sticker() { let mut stream = timeline.subscribe_events().await; timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": { "body": "Happy sticker", "info": { @@ -276,9 +276,9 @@ async fn test_internal_id_prefix() { let timeline = TestTimeline::with_internal_id_prefix("le_prefix_".to_owned()); let f = &timeline.factory; - let ev_a = f.text_msg("A").sender(*ALICE).into_sync(); - let ev_b = f.text_msg("B").sender(*BOB).into_sync(); - let ev_c = f.text_msg("C").sender(*CAROL).into_sync(); + let ev_a = f.text_msg("A").sender(*ALICE).into_event(); + let ev_b = f.text_msg("B").sender(*BOB).into_event(); + let ev_c = f.text_msg("C").sender(*CAROL).into_event(); timeline .controller @@ -445,7 +445,7 @@ async fn test_replace_with_initial_events_when_batched() { .with_settings(TimelineSettings::default()); let f = &timeline.factory; - let ev = f.text_msg("hey").sender(*ALICE).into_sync(); + let ev = f.text_msg("hey").sender(*ALICE).into_event(); timeline .controller @@ -460,7 +460,7 @@ async fn test_replace_with_initial_events_when_batched() { assert!(items[0].is_date_divider()); assert_eq!(items[1].as_event().unwrap().content().as_message().unwrap().body(), "hey"); - let ev = f.text_msg("yo").sender(*BOB).into_sync(); + let ev = f.text_msg("yo").sender(*BOB).into_event(); timeline .controller .replace_with_initial_remote_events([ev].into_iter(), RemoteEventOrigin::Sync) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs index f5a85de44..8d097f7d8 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/echo.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/echo.rs @@ -251,7 +251,7 @@ async fn test_no_read_marker_with_local_echo() { .sender(user_id!("@a:b.c")) .event_id(event_id) .server_ts(MilliSecondsSinceUnixEpoch::now()) - .into_sync()] + .into_event()] .into_iter(), RemoteEventOrigin::Sync, ) diff --git a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs index ed90d46c5..5c00e2186 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/edit.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/edit.rs @@ -19,7 +19,7 @@ use eyeball_im::VectorDiff; use matrix_sdk::deserialized_responses::{ AlgorithmInfo, EncryptionInfo, VerificationLevel, VerificationState, }; -use matrix_sdk_base::deserialized_responses::{DecryptedRoomEvent, SyncTimelineEvent}; +use matrix_sdk_base::deserialized_responses::{DecryptedRoomEvent, TimelineEvent}; use matrix_sdk_test::{async_test, ALICE}; use ruma::{ event_id, @@ -178,7 +178,7 @@ async fn test_edit_updates_encryption_info() { verification_state: VerificationState::Verified, }; - let original_event: SyncTimelineEvent = DecryptedRoomEvent { + let original_event: TimelineEvent = DecryptedRoomEvent { event: original_event.cast(), encryption_info: encryption_info.clone(), unsigned_encryption_info: None, @@ -207,7 +207,7 @@ async fn test_edit_updates_encryption_info() { .into_raw_timeline(); encryption_info.verification_state = VerificationState::Unverified(VerificationLevel::UnverifiedIdentity); - let edit_event: SyncTimelineEvent = DecryptedRoomEvent { + let edit_event: TimelineEvent = DecryptedRoomEvent { event: edit_event.cast(), encryption_info: encryption_info.clone(), unsigned_encryption_info: None, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs index d50fdb76f..40799138d 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/encryption.rs @@ -30,7 +30,7 @@ use matrix_sdk::{ crypto::{decrypt_room_key_export, types::events::UtdCause, OlmMachine}, test_utils::test_client_builder, }; -use matrix_sdk_base::deserialized_responses::{SyncTimelineEvent, UnableToDecryptReason}; +use matrix_sdk_base::deserialized_responses::{TimelineEvent, UnableToDecryptReason}; use matrix_sdk_test::{async_test, ALICE, BOB}; use ruma::{ assign, event_id, @@ -750,7 +750,7 @@ async fn test_retry_decryption_updates_response() { } } -fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { +fn utd_event_with_unsigned(unsigned: serde_json::Value) -> TimelineEvent { let raw = Raw::from_json( to_raw_value(&json!({ "event_id": "$myevent", @@ -770,7 +770,7 @@ fn utd_event_with_unsigned(unsigned: serde_json::Value) -> SyncTimelineEvent { .unwrap(), ); - SyncTimelineEvent::new_utd_event( + TimelineEvent::new_utd_event( raw, matrix_sdk::deserialized_responses::UnableToDecryptInfo { session_id: Some("SESSION_ID".into()), diff --git a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs index 5cfd714f6..da1d7d400 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; -use matrix_sdk::deserialized_responses::SyncTimelineEvent; +use matrix_sdk::deserialized_responses::TimelineEvent; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB}; use ruma::events::{ room::{ @@ -141,7 +141,7 @@ async fn test_hide_failed_to_parse() { // m.room.message events must have a msgtype and body in content, so this // event with an empty content object should fail to deserialize. timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": {}, "event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I", "origin_server_ts": 10, @@ -153,7 +153,7 @@ async fn test_hide_failed_to_parse() { // Similar to above, the m.room.member state event must also not have an // empty content object. timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": {}, "event_id": "$d5G0HA0FAZ37wP8kXlNkxx3I", "origin_server_ts": 2179, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/invalid.rs b/crates/matrix-sdk-ui/src/timeline/tests/invalid.rs index 8f3a08830..1340c6a09 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/invalid.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/invalid.rs @@ -14,7 +14,7 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; -use matrix_sdk::deserialized_responses::SyncTimelineEvent; +use matrix_sdk::deserialized_responses::TimelineEvent; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE, BOB}; use ruma::{ events::{room::message::MessageType, MessageLikeEventType, StateEventType}, @@ -60,7 +60,7 @@ async fn test_invalid_event_content() { // m.room.message events must have a msgtype and body in content, so this // event with an empty content object should fail to deserialize. timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": {}, "event_id": "$eeG0HA0FAZ37wP8kXlNkxx3I", "origin_server_ts": 10, @@ -79,7 +79,7 @@ async fn test_invalid_event_content() { // Similar to above, the m.room.member state event must also not have an // empty content object. timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": {}, "event_id": "$d5G0HA0FAZ37wP8kXlNkxx3I", "origin_server_ts": 2179, @@ -107,7 +107,7 @@ async fn test_invalid_event() { // This event is missing the sender field which the homeserver must add to // all timeline events. Because the event is malformed, it will be ignored. timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": { "body": "hello world", "msgtype": "m.text" diff --git a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs index 23bab244a..2c23569b2 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/mod.rs @@ -27,7 +27,7 @@ use futures_core::Stream; use indexmap::IndexMap; use matrix_sdk::{ config::RequestConfig, - deserialized_responses::{SyncTimelineEvent, TimelineEvent}, + deserialized_responses::TimelineEvent, event_cache::paginator::{PaginableRoom, PaginatorError}, room::{EventWithContextResponse, Messages, MessagesOptions}, send_queue::RoomSendQueueUpdate, @@ -173,7 +173,7 @@ impl TestTimeline { self.controller.items().await.len() } - async fn handle_live_event(&self, event: impl Into) { + async fn handle_live_event(&self, event: impl Into) { let event = event.into(); self.controller .add_events_at( @@ -297,7 +297,7 @@ impl PinnedEventsRoom for TestRoomDataProvider { _event_id: &'a EventId, _request_config: Option, _related_event_filters: Option>, - ) -> BoxFuture<'a, Result<(SyncTimelineEvent, Vec), PaginatorError>> { + ) -> BoxFuture<'a, Result<(TimelineEvent, Vec), PaginatorError>> { unimplemented!(); } diff --git a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs index cea3ffc18..78ca0d69b 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/reactions.rs @@ -18,7 +18,7 @@ use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_core::Stream; use futures_util::{FutureExt as _, StreamExt as _}; -use matrix_sdk::deserialized_responses::SyncTimelineEvent; +use matrix_sdk::deserialized_responses::TimelineEvent; use matrix_sdk_test::{async_test, event_factory::EventFactory, sync_timeline_event, ALICE, BOB}; use ruma::{ event_id, events::AnyMessageLikeEventContent, server_name, uint, EventId, @@ -151,7 +151,7 @@ async fn test_redact_reaction_success() { // When that redaction is confirmed by the server, timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "sender": *ALICE, "type": "m.room.redaction", "event_id": "$idb", @@ -198,9 +198,9 @@ async fn test_initial_reaction_timestamp_is_stored() { // Reaction comes first. f.reaction(&message_event_id, REACTION_KEY) .server_ts(reaction_timestamp) - .into_sync(), + .into_event(), // Event comes next. - f.text_msg("A").event_id(&message_event_id).into_sync(), + f.text_msg("A").event_id(&message_event_id).into_event(), ] .into_iter(), TimelineNewItemPosition::End { origin: RemoteEventOrigin::Sync }, diff --git a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs index c7f50c267..4dd6cb985 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/shields.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/shields.rs @@ -1,6 +1,6 @@ use assert_matches::assert_matches; use eyeball_im::VectorDiff; -use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode, SyncTimelineEvent}; +use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode, TimelineEvent}; use matrix_sdk_test::{async_test, sync_timeline_event, ALICE}; use ruma::{ event_id, @@ -97,7 +97,7 @@ async fn test_local_sent_in_clear_shield() { // When the remote echo comes in. timeline - .handle_live_event(SyncTimelineEvent::new(sync_timeline_event!({ + .handle_live_event(TimelineEvent::new(sync_timeline_event!({ "content": { "body": "Local message", "msgtype": "m.text", diff --git a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs index 9cb4b3fdb..cb2adf7d7 100644 --- a/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs +++ b/crates/matrix-sdk-ui/src/unable_to_decrypt_hook.rs @@ -27,6 +27,7 @@ use growable_bloom_filter::{GrowableBloom, GrowableBloomBuilder}; use matrix_sdk::{ crypto::types::events::UtdCause, executor::{spawn, JoinHandle}, + sleep::sleep, Client, }; use matrix_sdk_base::{StateStoreDataKey, StateStoreDataValue, StoreError}; @@ -34,10 +35,7 @@ use ruma::{ time::{Duration, Instant}, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedServerName, UserId, }; -use tokio::{ - sync::{Mutex as AsyncMutex, MutexGuard}, - time::sleep, -}; +use tokio::sync::{Mutex as AsyncMutex, MutexGuard}; use tracing::error; /// A generic interface which methods get called whenever we observe a diff --git a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs index 299dd3f84..6c79c0830 100644 --- a/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/encryption_sync_service.rs @@ -8,8 +8,8 @@ use std::{ use futures_util::{pin_mut, StreamExt as _}; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{logged_in_client_with_server, test_client_builder_with_server}, SessionMeta, }; diff --git a/crates/matrix-sdk-ui/tests/integration/main.rs b/crates/matrix-sdk-ui/tests/integration/main.rs index 9eb3b40e1..2c575ca81 100644 --- a/crates/matrix-sdk-ui/tests/integration/main.rs +++ b/crates/matrix-sdk-ui/tests/integration/main.rs @@ -55,6 +55,7 @@ async fn mock_sync(server: &MockServer, response_body: impl Serialize, since: Op /// /// Note: pass `events_before` in the normal order, I'll revert the order for /// you. +// TODO: replace with MatrixMockServer #[allow(clippy::too_many_arguments)] // clippy you've got such a fixed mindset async fn mock_context( server: &MockServer, @@ -86,6 +87,7 @@ async fn mock_context( /// /// Note: pass `chunk` in the correct order: topological for forward pagination, /// reverse topological for backwards pagination. +// TODO: replace with MatrixMockServer async fn mock_messages( server: &MockServer, start: String, diff --git a/crates/matrix-sdk-ui/tests/integration/sync_service.rs b/crates/matrix-sdk-ui/tests/integration/sync_service.rs index dae23a426..6c28f3190 100644 --- a/crates/matrix-sdk-ui/tests/integration/sync_service.rs +++ b/crates/matrix-sdk-ui/tests/integration/sync_service.rs @@ -73,19 +73,19 @@ async fn test_sync_service_state() -> anyhow::Result<()> { // At first, the sync service is sleeping. assert_eq!(state_stream.get(), State::Idle); assert!(server.received_requests().await.unwrap().is_empty()); - assert_eq!(sync_service.task_states(), (false, false)); + assert!(!sync_service.is_supervisor_running().await); assert!(sync_service.try_get_encryption_sync_permit().is_some()); // After starting, the sync service is, well, running. sync_service.start().await; assert_next_matches!(state_stream, State::Running); - assert_eq!(sync_service.task_states(), (true, true)); + assert!(sync_service.is_supervisor_running().await); assert!(sync_service.try_get_encryption_sync_permit().is_none()); // Restarting while started doesn't change the current state. sync_service.start().await; assert_pending!(state_stream); - assert_eq!(sync_service.task_states(), (true, true)); + assert!(sync_service.is_supervisor_running().await); assert!(sync_service.try_get_encryption_sync_permit().is_none()); // Let the server respond a few times. @@ -94,7 +94,7 @@ async fn test_sync_service_state() -> anyhow::Result<()> { // Pausing will stop both syncs, after a bit of delay. sync_service.stop().await?; assert_next_matches!(state_stream, State::Idle); - assert_eq!(sync_service.task_states(), (false, false)); + assert!(!sync_service.is_supervisor_running().await); assert!(sync_service.try_get_encryption_sync_permit().is_some()); let mut num_encryption_sync_requests: i32 = 0; @@ -149,7 +149,7 @@ async fn test_sync_service_state() -> anyhow::Result<()> { // the same position than just before being stopped. sync_service.start().await; assert_next_matches!(state_stream, State::Running); - assert_eq!(sync_service.task_states(), (true, true)); + assert!(sync_service.is_supervisor_running().await); assert!(sync_service.try_get_encryption_sync_permit().is_none()); tokio::time::sleep(Duration::from_millis(100)).await; diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs index 2d715963a..0bc4c57d6 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/echo.rs @@ -19,8 +19,8 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ - assert_next_matches_with_timeout, config::SyncSettings, executor::spawn, - ruma::MilliSecondsSinceUnixEpoch, test_utils::logged_in_client_with_server, + config::SyncSettings, executor::spawn, ruma::MilliSecondsSinceUnixEpoch, + test_utils::logged_in_client_with_server, }; use matrix_sdk_test::{ async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, @@ -33,7 +33,7 @@ use ruma::{ room_id, uint, user_id, }; use serde_json::json; -use stream_assert::assert_next_matches; +use stream_assert::{assert_next_matches, assert_pending}; use tokio::task::yield_now; use wiremock::{ matchers::{header, method, path_regex}, @@ -83,7 +83,10 @@ async fn test_echo() { timeline.send(RoomMessageEventContent::text_plain("Hello, World!").into()).await }); - assert_let!(Some(VectorDiff::PushBack { value: local_echo }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: local_echo } = &timeline_updates[0]); let item = local_echo.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); assert_let!(TimelineItemContent::Message(msg) = item.content()); @@ -92,15 +95,16 @@ async fn test_echo() { assert!(item.event_id().is_none()); let txn_id = item.transaction_id().unwrap(); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); // Wait for the sending to finish and assert everything was successful send_hdl.await.unwrap().unwrap(); - assert_let!( - Some(VectorDiff::Set { index: 1, value: sent_confirmation }) = timeline_stream.next().await - ); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: sent_confirmation } = &timeline_updates[0]); let item = sent_confirmation.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::Sent { .. })); assert_eq!(item.event_id(), Some(event_id)); @@ -120,19 +124,24 @@ async fn test_echo() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + // Local echo is replaced with the remote echo. - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 1 }); - let remote_echo = - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => value); + assert_let!(VectorDiff::Remove { index: 1 } = &timeline_updates[0]); + + assert_let!(VectorDiff::PushFront { value: remote_echo } = &timeline_updates[1]); let item = remote_echo.as_event().unwrap(); assert!(item.is_own()); assert_eq!(item.timestamp(), MilliSecondsSinceUnixEpoch(uint!(152038280))); // The date divider is also replaced. - let date_divider = - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => value); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[2]); assert!(date_divider.is_date_divider()); - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); + + assert_let!(VectorDiff::Remove { index: 2 } = &timeline_updates[3]); + + assert_pending!(timeline_stream); } #[async_test] @@ -251,14 +260,16 @@ async fn test_dedup_by_event_id_late() { timeline.send(RoomMessageEventContent::text_plain("Hello, World!").into()).await.unwrap(); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Timeline: [local echo] - let local_echo = - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(VectorDiff::PushBack { value: local_echo } = &timeline_updates[0]); let item = local_echo.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); // Timeline: [date-divider, local echo] - let date_divider = assert_next_matches_with_timeout!( timeline_stream, VectorDiff::PushFront { value } => value); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); let f = EventFactory::new(); @@ -275,21 +286,29 @@ async fn test_dedup_by_event_id_late() { mock_sync(&server, sync_builder.build_json_sync_response(), None).await; let _response = client.sync_once(sync_settings.clone()).await.unwrap(); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Timeline: [remote-echo, date-divider, local echo] - let remote_echo = - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => value); + assert_let!(VectorDiff::PushFront { value: remote_echo } = &timeline_updates[0]); let item = remote_echo.as_event().unwrap(); assert_eq!(item.event_id(), Some(event_id)); // Timeline: [date-divider, remote-echo, date-divider, local echo] - let date_divider = assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => value); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Local echo and its date divider are removed. // Timeline: [date-divider, remote-echo, date-divider] - assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 3 })); + assert_let!(VectorDiff::Remove { index: 3 } = &timeline_updates[0]); + // Timeline: [date-divider, remote-echo] - assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); + assert_let!(VectorDiff::Remove { index: 2 } = &timeline_updates[1]); + + assert_pending!(timeline_stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index 66619fdac..c53decc70 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -58,7 +58,7 @@ use ruma::{ OwnedRoomId, }; use serde_json::json; -use stream_assert::assert_next_matches; +use stream_assert::{assert_next_matches, assert_pending}; use tokio::{task::yield_now, time::sleep}; use wiremock::{ matchers::{header, method, path_regex}, @@ -98,7 +98,10 @@ async fn test_edit() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); let item = first.as_event().unwrap(); assert_eq!(item.read_receipts().len(), 1, "implicit read receipt"); assert_matches!(item.latest_edit_json(), None); @@ -107,7 +110,7 @@ async fn test_edit() { assert_matches!(msg.in_reply_to(), None); assert!(!msg.is_edited()); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); sync_builder.add_joined_room( @@ -124,7 +127,10 @@ async fn test_edit() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + + assert_let!(VectorDiff::PushBack { value: second } = &timeline_updates[0]); let item = second.as_event().unwrap(); assert!(item.event_id().is_some()); assert!(!item.is_own()); @@ -140,7 +146,7 @@ async fn test_edit() { // No more implicit read receipt in Alice's message, because they edited // something after the second event. - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[1]); let item = item.as_event().unwrap(); assert_matches!(item.latest_edit_json(), None); assert_let!(TimelineItemContent::Message(msg) = item.content()); @@ -151,7 +157,7 @@ async fn test_edit() { assert_eq!(item.read_receipts().len(), 0, "no more implicit read receipt"); // ... so Alice's read receipt moves to Bob's message. - assert_let!(Some(VectorDiff::Set { index: 2, value: second }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 2, value: second } = &timeline_updates[2]); let item = second.as_event().unwrap(); assert!(item.event_id().is_some()); assert!(!item.is_own()); @@ -159,7 +165,7 @@ async fn test_edit() { assert_eq!(item.read_receipts().len(), 2, "should carry alice and bob's read receipts"); // The text changes in Alice's message. - assert_let!(Some(VectorDiff::Set { index: 1, value: edit }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: edit } = &timeline_updates[3]); let item = edit.as_event().unwrap(); assert_matches!(item.latest_edit_json(), Some(_)); assert_let!(TimelineItemContent::Message(edited) = item.content()); @@ -189,19 +195,25 @@ async fn test_edit_local_echo() { // Redacting a local event works. timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap(); - assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); let internal_id = item.unique_id(); let item = item.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); // We haven't set a route for sending events, so this will fail. - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let item = item.as_event().unwrap(); assert!(item.is_local_echo()); @@ -212,7 +224,7 @@ async fn test_edit_local_echo() { Some(EventSendState::SendingFailed { is_recoverable: false, .. }) ); - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); // Set up the success response before editing, since edit causes an immediate // retry (the room's send queue is not blocked, since the one event it couldn't @@ -229,8 +241,11 @@ async fn test_edit_local_echo() { .await .unwrap(); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // Observe local echo being replaced. - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); assert_eq!(item.unique_id(), internal_id); @@ -246,8 +261,11 @@ async fn test_edit_local_echo() { // Re-enable the room's queue. timeline.room().send_queue().set_enabled(true); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // Observe the event being sent, and replacing the local echo. - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let item = item.as_event().unwrap(); assert!(item.is_local_echo()); @@ -256,7 +274,7 @@ async fn test_edit_local_echo() { assert_eq!(edit_message.body(), "hello, world"); // No new updates. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); } #[async_test] @@ -761,17 +779,22 @@ async fn test_edit_local_echo_with_unsupported_content() { timeline.send(RoomMessageEventContent::text_plain("hello, just you").into()).await.unwrap(); - assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); let item = item.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); // We haven't set a route for sending events, so this will fail. + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let item = item.as_event().unwrap(); assert!(item.is_local_echo()); @@ -782,7 +805,7 @@ async fn test_edit_local_echo_with_unsupported_content() { Some(EventSendState::SendingFailed { is_recoverable: false, .. }) ); - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); // Set up the success response before editing, since edit causes an immediate // retry (the room's send queue is not blocked, since the one event it couldn't @@ -814,7 +837,10 @@ async fn test_edit_local_echo_with_unsupported_content() { .await .unwrap(); - assert_let!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); let item = item.as_event().unwrap(); assert_matches!(item.send_state(), Some(EventSendState::NotSentYet)); @@ -832,6 +858,8 @@ async fn test_edit_local_echo_with_unsupported_content() { // We couldn't edit the local echo, since their content types didn't match assert_matches!(edit_err, Error::EditError(EditError::ContentMismatch { .. })); + + assert_pending!(timeline_stream); } struct PendingEditHelper { @@ -879,7 +907,7 @@ impl PendingEditHelper { .mount() .await; - self.timeline.live_paginate_backwards(batch_size).await.unwrap(); + self.timeline.paginate_backwards(batch_size).await.unwrap(); } } @@ -905,7 +933,7 @@ async fn test_pending_edit() { .await; // Nothing happens. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); // But when I receive the original event after a bit… h.handle_sync( @@ -914,8 +942,11 @@ async fn test_pending_edit() { ) .await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Then I get the edited content immediately. - assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); let event = value.as_event().unwrap(); let latest_edit_json = event.latest_edit_json().expect("we should have an edit json"); @@ -926,12 +957,11 @@ async fn test_pending_edit() { assert_eq!(msg.body(), "[edit]"); // The date divider. - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); + assert!(date_divider.is_date_divider()); // And nothing else. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); } #[async_test] @@ -963,7 +993,7 @@ async fn test_pending_edit_overrides() { .await; // Nothing happens. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); // And then I receive the original event after a bit… h.handle_sync( @@ -972,19 +1002,21 @@ async fn test_pending_edit_overrides() { ) .await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Then I get the latest edited content immediately. - assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); let msg = value.as_event().unwrap().content().as_message().unwrap(); assert!(msg.is_edited()); assert_eq!(msg.body(), "bonjour"); // The date divider. - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + assert!(value.is_date_divider()); // And nothing else. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); } #[async_test] @@ -1020,19 +1052,21 @@ async fn test_pending_edit_from_backpagination() { ) .await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Then I get the latest edited content immediately. - assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); let msg = value.as_event().unwrap().content().as_message().unwrap(); assert!(msg.is_edited()); assert_eq!(msg.body(), "hello"); // The date divider. - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + assert!(value.is_date_divider()); // And nothing else. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); } #[async_test] @@ -1073,7 +1107,7 @@ async fn test_pending_edit_from_backpagination_doesnt_override_pending_edit_from .await; // Nothing happens. - assert_matches!(timeline_stream.next().now_or_never(), None); + assert_pending!(timeline_stream); // And then I receive the original event after a bit… h.handle_sync( @@ -1082,20 +1116,22 @@ async fn test_pending_edit_from_backpagination_doesnt_override_pending_edit_from ) .await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Then I get the edit from the sync, even if the back-pagination happened // after. - assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); let msg = value.as_event().unwrap().content().as_message().unwrap(); assert!(msg.is_edited()); assert_eq!(msg.body(), "[edit]"); // The date divider. - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + assert!(value.is_date_divider()); // And nothing else. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); } #[async_test] @@ -1131,7 +1167,7 @@ async fn test_pending_poll_edit() { .await; // Nothing happens. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); // But when I receive the original event after a bit… let event_content = NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new( @@ -1149,8 +1185,11 @@ async fn test_pending_poll_edit() { ) .await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Then I get the edited content immediately. - assert_let!(Some(VectorDiff::PushBack { value }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); let poll = as_variant!(value.as_event().unwrap().content(), TimelineItemContent::Poll).unwrap(); assert!(poll.is_edit()); @@ -1160,12 +1199,11 @@ async fn test_pending_poll_edit() { assert_eq!(results.answers[1].text, "No"); // The date divider. - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + assert!(value.is_date_divider()); // And nothing else. - assert!(timeline_stream.next().now_or_never().is_none()); + assert_pending!(timeline_stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs index ccb208742..f136a76bc 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/focus_event.rs @@ -19,10 +19,7 @@ use std::time::Duration; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; -use matrix_sdk::{ - assert_next_matches_with_timeout, config::SyncSettings, - test_utils::logged_in_client_with_server, -}; +use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, BOB, @@ -57,13 +54,13 @@ async fn test_new_focused() { target_event, Some("prev1".to_owned()), vec![ - f.text_msg("i tried so hard").sender(*ALICE).into_timeline(), - f.text_msg("and got so far").sender(*ALICE).into_timeline(), + f.text_msg("i tried so hard").sender(*ALICE).into_event(), + f.text_msg("and got so far").sender(*ALICE).into_event(), ], - f.text_msg("in the end").event_id(target_event).sender(*BOB).into_timeline(), + f.text_msg("in the end").event_id(target_event).sender(*BOB).into_event(), vec![ - f.text_msg("it doesn't even").sender(*ALICE).into_timeline(), - f.text_msg("matter").sender(*ALICE).into_timeline(), + f.text_msg("it doesn't even").sender(*ALICE).into_event(), + f.text_msg("matter").sender(*ALICE).into_event(), ], Some("next1".to_owned()), vec![], @@ -117,35 +114,39 @@ async fn test_new_focused() { None, vec![ // reversed manually here - f.text_msg("And even though I tried, it all fell apart").sender(*BOB).into_timeline(), - f.text_msg("I kept everything inside").sender(*BOB).into_timeline(), + f.text_msg("And even though I tried, it all fell apart").sender(*BOB).into_event(), + f.text_msg("I kept everything inside").sender(*BOB).into_event(), ], vec![], ) .await; - let hit_start = timeline.focused_paginate_backwards(20).await.unwrap(); + let hit_start = timeline.paginate_backwards(20).await.unwrap(); assert!(hit_start); server.reset().await; - assert_let!(Some(VectorDiff::PushFront { value: message }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + + assert_let!(VectorDiff::PushFront { value: message } = &timeline_updates[0]); assert_eq!( message.as_event().unwrap().content().as_message().unwrap().body(), "And even though I tried, it all fell apart" ); - assert_let!(Some(VectorDiff::PushFront { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: message } = &timeline_updates[1]); assert_eq!( message.as_event().unwrap().content().as_message().unwrap().body(), "I kept everything inside" ); // Date divider post processing. - assert_let!(Some(VectorDiff::PushFront { value: item }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: item } = &timeline_updates[2]); assert!(item.is_date_divider()); - assert_let!(Some(VectorDiff::Remove { index }) = timeline_stream.next().await); - assert_eq!(index, 3); + + assert_let!(VectorDiff::Remove { index } = &timeline_updates[3]); + assert_eq!(*index, 3); // Now trigger a forward pagination. mock_messages( @@ -153,25 +154,28 @@ async fn test_new_focused() { "next1".to_owned(), Some("next2".to_owned()), vec![ - f.text_msg("I had to fall, to lose it all").sender(*BOB).into_timeline(), - f.text_msg("But in the end, it doesn't event matter").sender(*BOB).into_timeline(), + f.text_msg("I had to fall, to lose it all").sender(*BOB).into_event(), + f.text_msg("But in the end, it doesn't event matter").sender(*BOB).into_event(), ], vec![], ) .await; - let hit_start = timeline.focused_paginate_forwards(20).await.unwrap(); + let hit_start = timeline.paginate_forwards(20).await.unwrap(); assert!(!hit_start); // because we gave it another next2 token. server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); assert_eq!( message.as_event().unwrap().content().as_message().unwrap().body(), "I had to fall, to lose it all" ); - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[1]); assert_eq!( message.as_event().unwrap().content().as_message().unwrap().body(), "But in the end, it doesn't event matter" @@ -204,7 +208,7 @@ async fn test_focused_timeline_reacts() { target_event, None, vec![], - f.text_msg("yolo").event_id(target_event).sender(*BOB).into_timeline(), + f.text_msg("yolo").event_id(target_event).sender(*BOB).into_event(), vec![], None, vec![], @@ -250,7 +254,10 @@ async fn test_focused_timeline_reacts() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - let item = assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index: 1, value: item } => item); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let event_item = item.as_event().unwrap(); // Text hasn't changed. @@ -286,7 +293,7 @@ async fn test_focused_timeline_local_echoes() { target_event, None, vec![], - f.text_msg("yolo").event_id(target_event).sender(*BOB).into_timeline(), + f.text_msg("yolo").event_id(target_event).sender(*BOB).into_event(), vec![], None, vec![], @@ -322,8 +329,11 @@ async fn test_focused_timeline_local_echoes() { // Add a reaction to the focused event, which will cause a local echo to happen. timeline.toggle_reaction(&event_item.identifier(), "✨").await.unwrap(); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // We immediately get the local echo for the reaction. - let item = assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index: 1, value: item } => item); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let event_item = item.as_event().unwrap(); // Text hasn't changed. @@ -362,7 +372,7 @@ async fn test_focused_timeline_doesnt_show_local_echoes() { target_event, None, vec![], - f.text_msg("yolo").event_id(target_event).sender(*BOB).into_timeline(), + f.text_msg("yolo").event_id(target_event).sender(*BOB).into_event(), vec![], None, vec![], diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index a54081b95..23b065f97 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -19,7 +19,6 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use futures_util::StreamExt; use matrix_sdk::{ - assert_let_timeout, config::SyncSettings, test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, }; @@ -40,7 +39,7 @@ use ruma::{ owned_event_id, room_id, user_id, MilliSecondsSinceUnixEpoch, }; use serde_json::json; -use stream_assert::{assert_next_matches, assert_pending}; +use stream_assert::assert_pending; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, @@ -113,17 +112,18 @@ async fn test_reaction() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + // The new message starts with their author's read receipt. - assert_let_timeout!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next()); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); let event_item = message.as_event().unwrap(); assert_matches!(event_item.content(), TimelineItemContent::Message(_)); assert_eq!(event_item.read_receipts().len(), 1); // The new message is getting the reaction, which implies an implicit read // receipt that's obtained first. - assert_let_timeout!( - Some(VectorDiff::Set { index: 0, value: updated_message }) = timeline_stream.next() - ); + assert_let!(VectorDiff::Set { index: 0, value: updated_message } = &timeline_updates[1]); let event_item = updated_message.as_event().unwrap(); assert_let!(TimelineItemContent::Message(msg) = event_item.content()); assert!(!msg.is_edited()); @@ -131,9 +131,7 @@ async fn test_reaction() { assert_eq!(event_item.reactions().len(), 0); // Then the reaction is taken into account. - assert_let_timeout!( - Some(VectorDiff::Set { index: 0, value: updated_message }) = timeline_stream.next() - ); + assert_let!(VectorDiff::Set { index: 0, value: updated_message } = &timeline_updates[2]); let event_item = updated_message.as_event().unwrap(); assert_let!(TimelineItemContent::Message(msg) = event_item.content()); assert!(!msg.is_edited()); @@ -145,9 +143,7 @@ async fn test_reaction() { assert_eq!(senders.as_slice(), [user_id!("@bob:example.org")]); // The date divider. - assert_let_timeout!( - Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next() - ); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[3]); assert!(date_divider.is_date_divider()); sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( @@ -165,13 +161,16 @@ async fn test_reaction() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let_timeout!( - Some(VectorDiff::Set { index: 1, value: updated_message }) = timeline_stream.next() - ); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: updated_message } = &timeline_updates[0]); let event_item = updated_message.as_event().unwrap(); assert_let!(TimelineItemContent::Message(msg) = event_item.content()); assert!(!msg.is_edited()); assert_eq!(event_item.reactions().len(), 0); + + assert_pending!(timeline_stream); } #[async_test] @@ -226,11 +225,16 @@ async fn test_redacted_message() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); assert_matches!(first.as_event().unwrap().content(), TimelineItemContent::RedactedMessage); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); + + assert_pending!(timeline_stream); } #[async_test] @@ -258,13 +262,16 @@ async fn test_redact_message() { ) .await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); assert_eq!( first.as_event().unwrap().content().as_message().unwrap().body(), "buy my bitcoins bro" ); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); // Redacting a remote event works. @@ -278,14 +285,20 @@ async fn test_redact_message() { .await .unwrap(); - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::PushBack { value: second } = &timeline_updates[0]); let second = second.as_event().unwrap(); assert_matches!(second.send_state(), Some(EventSendState::NotSentYet)); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // We haven't set a route for sending events, so this will fail. - assert_let!(Some(VectorDiff::Set { index, value: second }) = timeline_stream.next().await); - assert_eq!(index, 2); + assert_let!(VectorDiff::Set { index, value: second } = &timeline_updates[0]); + assert_eq!(*index, 2); let second = second.as_event().unwrap(); assert!(second.is_local_echo()); @@ -294,8 +307,13 @@ async fn test_redact_message() { // Let's redact the local echo. timeline.redact(&second.identifier(), None).await.unwrap(); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // Observe local echo being removed. - assert_matches!(timeline_stream.next().await, Some(VectorDiff::Remove { index: 2 })); + assert_let!(VectorDiff::Remove { index: 2 } = &timeline_updates[0]); + + assert_pending!(timeline_stream); } #[async_test] @@ -320,21 +338,27 @@ async fn test_redact_local_sent_message() { .await .unwrap(); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Assert the local event is in the timeline now and is not sent yet. - assert_let_timeout!(Some(VectorDiff::PushBack { value: item }) = timeline_stream.next()); + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); let event = item.as_event().unwrap(); assert!(event.is_local_echo()); assert_matches!(event.send_state(), Some(EventSendState::NotSentYet)); // As well as a date divider. - assert_let_timeout!( - Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next() - ); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // We receive an update in the timeline from the send queue. - assert_let_timeout!(Some(VectorDiff::Set { index, value: item }) = timeline_stream.next()); - assert_eq!(index, 1); + assert_let!(VectorDiff::Set { index, value: item } = &timeline_updates[0]); + assert_eq!(*index, 1); + + assert_pending!(timeline_stream); // Check the event is sent but still considered local. let event = item.as_event().unwrap(); @@ -415,10 +439,13 @@ async fn test_read_marker() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); assert_matches!(message.as_event().unwrap().content(), TimelineItemContent::Message(_)); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); sync_builder.add_joined_room( @@ -448,13 +475,16 @@ async fn test_read_marker() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); assert_matches!(message.as_event().unwrap().content(), TimelineItemContent::Message(_)); - assert_let!( - Some(VectorDiff::Insert { index: 2, value: marker }) = timeline_stream.next().await - ); + assert_let!(VectorDiff::Insert { index: 2, value: marker } = &timeline_updates[1]); assert_matches!(marker.as_virtual().unwrap(), VirtualTimelineItem::ReadMarker); + + assert_pending!(timeline_stream); } #[async_test] @@ -499,12 +529,15 @@ async fn test_sync_highlighted() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); let remote_event = first.as_event().unwrap(); // Own events don't trigger push rules. assert!(!remote_event.is_highlighted()); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); sync_builder.add_joined_room(JoinedRoomBuilder::new(room_id).add_timeline_event( @@ -525,10 +558,15 @@ async fn test_sync_highlighted() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::PushBack { value: second } = &timeline_updates[0]); let remote_event = second.as_event().unwrap(); // `m.room.tombstone` should be highlighted by default. assert!(remote_event.is_highlighted()); + + assert_pending!(timeline_stream); } #[async_test] @@ -772,21 +810,24 @@ async fn test_timeline_without_encryption_can_update() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 3); + // Previous timeline event now has a shield - assert_next_matches!(stream, VectorDiff::Set { index, value } => { - assert_eq!(index, 1); - assert!(value.as_event().unwrap().get_shield(false).is_some()); - }); + assert_let!(VectorDiff::Set { index, value } = &timeline_updates[0]); + assert_eq!(*index, 1); + assert!(value.as_event().unwrap().get_shield(false).is_some()); + // Room encryption event is received - assert_next_matches!(stream, VectorDiff::PushBack { value } => { - assert_let!(TimelineItemContent::OtherState(other_state) = value.as_event().unwrap().content()); - assert_let!(AnyOtherFullStateEventContent::RoomEncryption(_) = other_state.content()); - assert!(value.as_event().unwrap().get_shield(false).is_some()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + assert_let!(TimelineItemContent::OtherState(other_state) = value.as_event().unwrap().content()); + assert_let!(AnyOtherFullStateEventContent::RoomEncryption(_) = other_state.content()); + assert!(value.as_event().unwrap().get_shield(false).is_some()); + // New message event is received and has a shield - assert_next_matches!(stream, VectorDiff::PushBack { value } => { - assert!(value.as_event().unwrap().get_shield(false).is_some()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); + assert!(value.as_event().unwrap().get_shield(false).is_some()); + assert_pending!(stream); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs index deea9e419..e8b5c5b3b 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pagination.rs @@ -76,7 +76,7 @@ async fn test_back_pagination() { .await; let paginate = async { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); server.reset().await; }; let observe_paginating = async { @@ -84,9 +84,11 @@ async fn test_back_pagination() { }; join(paginate, observe_paginating).await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + // `m.room.name` { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); assert_eq!(state.state_key(), ""); assert_let!( @@ -101,13 +103,13 @@ async fn test_back_pagination() { // `m.room.name` receives an update { - assert_let!(Some(VectorDiff::Set { index, .. }) = timeline_stream.next().await); - assert_eq!(index, 0); + assert_let!(VectorDiff::Set { index, .. } = &timeline_updates[1]); + assert_eq!(*index, 0); } // `m.room.message`: “the world is big” { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[2]); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "the world is big"); @@ -115,7 +117,7 @@ async fn test_back_pagination() { // `m.room.message`: “hello world” { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[3]); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "hello world"); @@ -123,9 +125,7 @@ async fn test_back_pagination() { // Date divider is updated. { - assert_let!( - Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await - ); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[4]); assert!(date_divider.is_date_divider()); } @@ -145,7 +145,7 @@ async fn test_back_pagination() { .mount(&server) .await; - let hit_start = timeline.live_paginate_backwards(10).await.unwrap(); + let hit_start = timeline.paginate_backwards(10).await.unwrap(); assert!(hit_start); assert_next_eq!( back_pagination_status, @@ -218,12 +218,14 @@ async fn test_back_pagination_highlighted() { .mount(&server) .await; - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + // `m.room.tombstone` { - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: second } = &timeline_updates[0]); let remote_event = second.as_event().unwrap(); // `m.room.tombstone` should be highlighted by default. assert!(remote_event.is_highlighted()); @@ -231,7 +233,7 @@ async fn test_back_pagination_highlighted() { // `m.room.message` { - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[1]); let remote_event = first.as_event().unwrap(); // Own events don't trigger push rules. assert!(!remote_event.is_highlighted()); @@ -239,9 +241,7 @@ async fn test_back_pagination_highlighted() { // Date divider { - assert_let!( - Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await - ); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[2]); assert!(date_divider.is_date_divider()); } @@ -289,7 +289,7 @@ async fn test_wait_for_token() { mock_sync(&server, sync_builder.build_json_sync_response(), None).await; let paginate = async { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); }; let observe_paginating = async { assert_eq!(back_pagination_status.next().await, Some(LiveBackPaginationStatus::Paginating)); @@ -354,10 +354,10 @@ async fn test_dedup_pagination() { // If I try to paginate twice at the same time, let paginate_1 = async { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); }; let paginate_2 = async { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); }; timeout(Duration::from_secs(5), join(paginate_1, paginate_2)).await.unwrap(); @@ -443,7 +443,7 @@ async fn test_timeline_reset_while_paginating() { let (_, mut back_pagination_status) = timeline.live_back_pagination_status().await.unwrap(); - let paginate = async { timeline.live_paginate_backwards(10).await.unwrap() }; + let paginate = async { timeline.paginate_backwards(10).await.unwrap() }; let observe_paginating = async { let mut seen_paginating = false; @@ -608,7 +608,7 @@ async fn test_empty_chunk() { .await; let paginate = async { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); server.reset().await; }; let observe_paginating = async { @@ -616,9 +616,11 @@ async fn test_empty_chunk() { }; join(paginate, observe_paginating).await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + // `m.room.name` { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); assert_eq!(state.state_key(), ""); assert_let!( @@ -633,13 +635,13 @@ async fn test_empty_chunk() { // `m.room.name` is updated { - assert_let!(Some(VectorDiff::Set { index, .. }) = timeline_stream.next().await); - assert_eq!(index, 0); + assert_let!(VectorDiff::Set { index, .. } = &timeline_updates[1]); + assert_eq!(*index, 0); } // `m.room.message`: “the world is big” { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[2]); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "the world is big"); @@ -647,7 +649,7 @@ async fn test_empty_chunk() { // `m.room.name`: “hello world” { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[3]); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "hello world"); @@ -655,9 +657,7 @@ async fn test_empty_chunk() { // Date divider { - assert_let!( - Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await - ); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[4]); assert!(date_divider.is_date_divider()); } @@ -719,16 +719,18 @@ async fn test_until_num_items_with_empty_chunk() { .await; let paginate = async { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); }; let observe_paginating = async { assert_eq!(back_pagination_status.next().await, Some(LiveBackPaginationStatus::Paginating)); }; join(paginate, observe_paginating).await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + // `m.room.name` { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[0]); assert_let!(TimelineItemContent::OtherState(state) = message.as_event().unwrap().content()); assert_eq!(state.state_key(), ""); assert_let!( @@ -743,13 +745,13 @@ async fn test_until_num_items_with_empty_chunk() { // `m.room.name` is updated { - assert_let!(Some(VectorDiff::Set { index, .. }) = timeline_stream.next().await); - assert_eq!(index, 0); + assert_let!(VectorDiff::Set { index, .. } = &timeline_updates[1]); + assert_eq!(*index, 0); } // `m.room.message`: “the world is big” { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[2]); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "the world is big"); @@ -757,7 +759,7 @@ async fn test_until_num_items_with_empty_chunk() { // `m.room.name`: “hello world” { - assert_let!(Some(VectorDiff::PushBack { value: message }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: message } = &timeline_updates[3]); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "hello world"); @@ -765,20 +767,18 @@ async fn test_until_num_items_with_empty_chunk() { // Date divider { - assert_let!( - Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await - ); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[4]); assert!(date_divider.is_date_divider()); } - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); + + assert_let!(Some(timeline_updates) = timeline_stream.next().await); // `m.room.name`: “hello room then” { - assert_let!( - Some(VectorDiff::Insert { index, value: message }) = timeline_stream.next().await - ); - assert_eq!(index, 1); + assert_let!(VectorDiff::Insert { index, value: message } = &timeline_updates[0]); + assert_eq!(*index, 1); assert_let!(TimelineItemContent::Message(msg) = message.as_event().unwrap().content()); assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "hello room then"); @@ -821,7 +821,7 @@ async fn test_back_pagination_aborted() { let paginate = spawn({ let timeline = timeline.clone(); async move { - timeline.live_paginate_backwards(10).await.unwrap(); + timeline.paginate_backwards(10).await.unwrap(); } }); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs index dfbf12b26..4fc29373f 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/pinned_event.rs @@ -1,9 +1,10 @@ use std::{ops::ControlFlow, time::Duration}; use assert_matches::assert_matches; +use assert_matches2::assert_let; use eyeball_im::VectorDiff; +use futures_util::StreamExt as _; use matrix_sdk::{ - assert_next_matches_with_timeout, config::SyncSettings, event_cache::{BackPaginationOutcome, TimelineHasBeenResetWhilePaginating}, test_utils::{ @@ -106,22 +107,32 @@ async fn test_new_pinned_events_are_added_on_sync() { .await .expect("Room should be synced"); + // If the test runs fast, we receive 1 update, then 4 updates. If the test runs + // slow, we receive 5 updates directly. Let's solve this flakiness with a + // `sleep`. + sleep(Duration::from_millis(500)).await; + + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 5); + // The item is added automatically - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); + assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); + // The list is reloaded, so it's reset - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); + assert_let!(VectorDiff::Clear = &timeline_updates[1]); + // Then the loaded list items are added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$1")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); + assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$1")); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[3]); + assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[4]); + assert!(value.is_date_divider()); + + assert_pending!(timeline_stream); } #[async_test] @@ -175,16 +186,20 @@ async fn test_new_pinned_event_ids_reload_the_timeline() { .await .expect("Sync failed"); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$1")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + + assert_let!(VectorDiff::Clear = &timeline_updates[0]); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$1")); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); + assert_eq!(value.as_event().unwrap().event_id().unwrap(), event_id!("$2")); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[3]); + assert!(value.is_date_divider()); + assert_pending!(timeline_stream); // Reload timeline with no pinned event @@ -195,7 +210,10 @@ async fn test_new_pinned_event_ids_reload_the_timeline() { .await .expect("Sync failed"); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + assert_let!(VectorDiff::Clear = &timeline_updates[0]); + assert_pending!(timeline_stream); } @@ -532,26 +550,30 @@ async fn test_edited_events_are_reflected_in_sync() { .await .expect("Sync failed"); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + // The list is reloaded, so it's reset - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); + assert_let!(VectorDiff::Clear = &timeline_updates[0]); + // Then the loaded list items are added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - let event = value.as_event().unwrap(); - assert_eq!(event.event_id().unwrap(), event_id!("$1")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + let event = value.as_event().unwrap(); + assert_eq!(event.event_id().unwrap(), event_id!("$1")); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[2]); + assert!(value.is_date_divider()); + // The edit replaces the original event - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { - assert_eq!(index, 1); - match value.as_event().unwrap().content() { - TimelineItemContent::Message(m) => { - assert_eq!(m.body(), "* edited message!") - } - _ => panic!("Should be a message event"), + assert_let!(VectorDiff::Set { index, value } = &timeline_updates[3]); + assert_eq!(*index, 1); + match value.as_event().unwrap().content() { + TimelineItemContent::Message(m) => { + assert_eq!(m.body(), "* edited message!") } - }); + _ => panic!("Should be a message event"), + } + assert_pending!(timeline_stream); } @@ -610,21 +632,25 @@ async fn test_redacted_events_are_reflected_in_sync() { .await .expect("Sync failed"); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + // The list is reloaded, so it's reset - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); + assert_let!(VectorDiff::Clear = &timeline_updates[0]); + // Then the loaded list items are added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - let event = value.as_event().unwrap(); - assert_eq!(event.event_id().unwrap(), event_id!("$1")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + let event = value.as_event().unwrap(); + assert_eq!(event.event_id().unwrap(), event_id!("$1")); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[2]); + assert!(value.is_date_divider()); + // The redaction replaces the original event - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { - assert_eq!(index, 1); - assert_matches!(value.as_event().unwrap().content(), TimelineItemContent::RedactedMessage); - }); + assert_let!(VectorDiff::Set { index, value } = &timeline_updates[3]); + assert_eq!(*index, 1); + assert_matches!(value.as_event().unwrap().content(), TimelineItemContent::RedactedMessage); + assert_pending!(timeline_stream); } @@ -687,26 +713,30 @@ async fn test_edited_events_survive_pinned_event_ids_change() { .await .expect("Sync failed"); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + // The list is reloaded, so it's reset - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); + assert_let!(VectorDiff::Clear = &timeline_updates[0]); + // Then the loaded list items are added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - let event = value.as_event().unwrap(); - assert_eq!(event.event_id().unwrap(), event_id!("$1")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + let event = value.as_event().unwrap(); + assert_eq!(event.event_id().unwrap(), event_id!("$1")); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[2]); + assert!(value.is_date_divider()); + // The edit replaces the original event - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { - assert_eq!(index, 1); - match value.as_event().unwrap().content() { - TimelineItemContent::Message(m) => { - assert_eq!(m.body(), "edited message!") - } - _ => panic!("Should be a message event"), + assert_let!(VectorDiff::Set { index, value } = &timeline_updates[3]); + assert_eq!(*index, 1); + match value.as_event().unwrap().content() { + TimelineItemContent::Message(m) => { + assert_eq!(m.body(), "edited message!") } - }); + _ => panic!("Should be a message event"), + } + assert_pending!(timeline_stream); let new_pinned_event = f @@ -726,36 +756,44 @@ async fn test_edited_events_survive_pinned_event_ids_change() { .await .expect("Sync failed"); + // If the test runs fast, we receive 1 update, then 5 updates. If the test runs + // slow, we receive 6 updates directly. Let's solve this flakiness with a + // `sleep`. + sleep(Duration::from_millis(500)).await; + + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 6); + // New item gets added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - let event = value.as_event().unwrap(); - assert_eq!(event.event_id().unwrap(), event_id!("$3")); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); + let event = value.as_event().unwrap(); + assert_eq!(event.event_id().unwrap(), event_id!("$3")); + // The list is reloaded, so it's reset - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Clear); + assert_let!(VectorDiff::Clear = &timeline_updates[1]); + // Then the loaded list items are added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - let event = value.as_event().unwrap(); - assert_eq!(event.event_id().unwrap(), event_id!("$1")); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); + let event = value.as_event().unwrap(); + assert_eq!(event.event_id().unwrap(), event_id!("$1")); + // The edit replaces the original event - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::Set { index, value } => { - assert_eq!(index, 0); - match value.as_event().unwrap().content() { - TimelineItemContent::Message(m) => { - assert_eq!(m.body(), "edited message!") - } - _ => panic!("Should be a message event"), + assert_let!(VectorDiff::Set { index, value } = &timeline_updates[3]); + assert_eq!(*index, 0); + match value.as_event().unwrap().content() { + TimelineItemContent::Message(m) => { + assert_eq!(m.body(), "edited message!") } - }); + _ => panic!("Should be a message event"), + } // The new pinned event is added - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushBack { value } => { - let event = value.as_event().unwrap(); - assert_eq!(event.event_id().unwrap(), event_id!("$3")); - }); - assert_next_matches_with_timeout!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[4]); + let event = value.as_event().unwrap(); + assert_eq!(event.event_id().unwrap(), event_id!("$3")); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[5]); + assert!(value.is_date_divider()); + assert_pending!(timeline_stream); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs index 526ce50d8..4a7ae1f08 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/queue.rs @@ -335,9 +335,14 @@ async fn test_clear_with_echoes() { // Wait for the first message to fail. Don't use time, but listen for the first // timeline item diff to get back signalling the error. - let _date_divider = timeline_stream.next().await; - let _local_echo = timeline_stream.next().await; - let _local_echo_replaced_with_failure = timeline_stream.next().await; + + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + // 2 updates: date divider and local echo. + assert_eq!(timeline_updates.len(), 2); + + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + // 1 updates: local echo replaced with failure. + assert_eq!(timeline_updates.len(), 1); } // Next message will take "forever" to send. @@ -440,35 +445,36 @@ async fn test_no_duplicate_date_divider() { // Let the send queue handle the event. yield_now().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 3); + // Local echoes are available as soon as `timeline.send` returns. - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().content().as_message().unwrap().body(), "First!"); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); + assert_eq!(value.as_event().unwrap().content().as_message().unwrap().body(), "First!"); - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + assert!(value.is_date_divider()); - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().content().as_message().unwrap().body(), "Second."); - }); + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]); + assert_eq!(value.as_event().unwrap().content().as_message().unwrap().body(), "Second."); // Wait 200ms for the first msg, 100ms for the second, 200ms for overhead. sleep(Duration::from_millis(500)).await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // The first item should be updated first. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => { - let value = value.as_event().unwrap(); - assert_eq!(value.content().as_message().unwrap().body(), "First!"); - assert_eq!(value.event_id().unwrap(), "$PyHxV5mYzjetBUT3qZq7V95GOzxb02EP"); - }); + assert_let!(VectorDiff::Set { index: 1, value } = &timeline_updates[0]); + let value = value.as_event().unwrap(); + assert_eq!(value.content().as_message().unwrap().body(), "First!"); + assert_eq!(value.event_id().unwrap(), "$PyHxV5mYzjetBUT3qZq7V95GOzxb02EP"); // Then the second one. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => { - let value = value.as_event().unwrap(); - assert_eq!(value.content().as_message().unwrap().body(), "Second."); - assert_eq!(value.event_id().unwrap(), "$5E2kLK/Sg342bgBU9ceEIEPYpbFaqJpZ"); - }); + assert_let!(VectorDiff::Set { index: 2, value } = &timeline_updates[1]); + let value = value.as_event().unwrap(); + assert_eq!(value.content().as_message().unwrap().body(), "Second."); + assert_eq!(value.event_id().unwrap(), "$5E2kLK/Sg342bgBU9ceEIEPYpbFaqJpZ"); assert_pending!(timeline_stream); @@ -496,31 +502,32 @@ async fn test_no_duplicate_date_divider() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 6); + // The first message is removed -> [DD Second] - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 1 }); + assert_let!(VectorDiff::Remove { index: 1 } = &timeline_updates[0]); // The first message is reinserted -> [First DD Second] - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - let value = value.as_event().unwrap(); - assert_eq!(value.content().as_message().unwrap().body(), "First!"); - assert_eq!(value.event_id().unwrap(), "$PyHxV5mYzjetBUT3qZq7V95GOzxb02EP"); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[1]); + let value = value.as_event().unwrap(); + assert_eq!(value.content().as_message().unwrap().body(), "First!"); + assert_eq!(value.event_id().unwrap(), "$PyHxV5mYzjetBUT3qZq7V95GOzxb02EP"); // The second message is replaced -> [First Second DD] - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 2 }); - assert_next_matches!(timeline_stream, VectorDiff::Insert { index: 1, value } => { - let value = value.as_event().unwrap(); - assert_eq!(value.content().as_message().unwrap().body(), "Second."); - assert_eq!(value.event_id().unwrap(), "$5E2kLK/Sg342bgBU9ceEIEPYpbFaqJpZ"); - }); + assert_let!(VectorDiff::Remove { index: 2 } = &timeline_updates[2]); + + assert_let!(VectorDiff::Insert { index: 1, value } = &timeline_updates[3]); + let value = value.as_event().unwrap(); + assert_eq!(value.content().as_message().unwrap().body(), "Second."); + assert_eq!(value.event_id().unwrap(), "$5E2kLK/Sg342bgBU9ceEIEPYpbFaqJpZ"); // A new date divider is inserted -> [DD First Second DD] - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[4]); + assert!(value.is_date_divider()); // The useless date divider is removed. -> [DD First Second] - assert_next_matches!(timeline_stream, VectorDiff::Remove { index: 3 }); + assert_let!(VectorDiff::Remove { index: 3 } = &timeline_updates[5]); assert_pending!(timeline_stream); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs index ff2e5bf33..24994a5be 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/reactions.rs @@ -17,10 +17,7 @@ use std::{sync::Mutex, time::Duration}; use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_util::{FutureExt as _, StreamExt as _}; -use matrix_sdk::{ - assert_next_matches_with_timeout, - test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}, -}; +use matrix_sdk::test_utils::{logged_in_client_with_server, mocks::MatrixMockServer}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, mocks::mock_encryption_state, JoinedRoomBuilder, SyncResponseBuilder, ALICE, @@ -28,6 +25,7 @@ use matrix_sdk_test::{ use matrix_sdk_ui::timeline::{ReactionStatus, RoomExt as _}; use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id}; use serde_json::json; +use stream_assert::assert_pending; use wiremock::{ matchers::{header, method, path_regex}, Mock, ResponseTemplate, @@ -64,12 +62,15 @@ async fn test_abort_before_being_sent() { ) .await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = stream.next().await); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); let item = first.as_event().unwrap(); let item_id = item.identifier(); assert_eq!(item.content().as_message().unwrap().body(), "hello"); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); // Now we try to add two reactions to this message… @@ -98,7 +99,10 @@ async fn test_abort_before_being_sent() { // First toggle (local echo). { - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let reactions = item.as_event().unwrap().reactions(); assert_eq!(reactions.len(), 1); @@ -107,14 +111,17 @@ async fn test_abort_before_being_sent() { ReactionStatus::LocalToRemote(_) ); - assert!(stream.next().now_or_never().is_none()); + assert_pending!(stream); } // We toggle another reaction at the same time… timeline.toggle_reaction(&item_id, "🥰").await.unwrap(); { - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let reactions = item.as_event().unwrap().reactions(); assert_eq!(reactions.len(), 2); @@ -135,7 +142,10 @@ async fn test_abort_before_being_sent() { timeline.toggle_reaction(&item_id, "👍").await.unwrap(); { - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let reactions = item.as_event().unwrap().reactions(); assert_eq!(reactions.len(), 1); @@ -152,7 +162,10 @@ async fn test_abort_before_being_sent() { timeline.toggle_reaction(&item_id, "🥰").await.unwrap(); { - assert_let!(Some(VectorDiff::Set { index: 1, value: item }) = stream.next().await); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); let reactions = item.as_event().unwrap().reactions(); assert!(reactions.is_empty()); @@ -165,7 +178,7 @@ async fn test_abort_before_being_sent() { // redaction of the reaction. In our case, we're done here. tokio::time::sleep(Duration::from_millis(300)).await; - assert!(stream.next().now_or_never().is_none()); + assert_pending!(stream); } #[async_test] @@ -206,20 +219,24 @@ async fn test_redact_failed() { let _response = client.sync_once(Default::default()).await.unwrap(); server.reset().await; - let item_id = assert_next_matches_with_timeout!(stream, VectorDiff::PushBack { value: item } => { + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 3); + + let item_id = { + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); + let item = item.as_event().unwrap(); assert_eq!(item.content().as_message().unwrap().body(), "hello"); assert!(item.reactions().is_empty()); + item.identifier() - }); + }; - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 0, value: item } => { - assert_eq!(item.as_event().unwrap().reactions().len(), 1); - }); + assert_let!(VectorDiff::Set { index: 0, value: item } = &timeline_updates[1]); + assert_eq!(item.as_event().unwrap().reactions().len(), 1); - assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: date_divider } => { - assert!(date_divider.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[2]); + assert!(date_divider.is_date_divider()); // Now, redact the annotation we previously added. @@ -235,18 +252,19 @@ async fn test_redact_failed() { // We toggle the reaction, which fails with an error. timeline.toggle_reaction(&item_id, "😆").await.unwrap_err(); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // The local echo is removed (assuming the redaction works)… - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { - assert!(item.as_event().unwrap().reactions().is_empty()); - }); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); + assert!(item.as_event().unwrap().reactions().is_empty()); // …then added back, after redaction failed. - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { - assert_eq!(item.as_event().unwrap().reactions().len(), 1); - }); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[1]); + assert_eq!(item.as_event().unwrap().reactions().len(), 1); tokio::time::sleep(Duration::from_millis(150)).await; - assert!(stream.next().now_or_never().is_none()); + assert_pending!(stream); } #[async_test] @@ -300,73 +318,88 @@ async fn test_local_reaction_to_local_echo() { // Send a local event. let _ = timeline.send(RoomMessageEventContent::text_plain("lol").into()).await.unwrap(); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Receive a local echo. - let item_id = assert_next_matches_with_timeout!(stream, VectorDiff::PushBack { value: item } => { + + let item_id = { + assert_let!(VectorDiff::PushBack { value: item } = &timeline_updates[0]); + let item = item.as_event().unwrap(); assert!(item.is_local_echo()); assert_eq!(item.content().as_message().unwrap().body(), "lol"); assert!(item.reactions().is_empty()); item.identifier() - }); + }; // Good ol' date divider. - assert_next_matches_with_timeout!(stream, VectorDiff::PushFront { value: date_divider } => { - assert!(date_divider.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); + assert!(date_divider.is_date_divider()); // Add a reaction before the remote echo comes back. let key1 = "🤣"; timeline.toggle_reaction(&item_id, key1).await.unwrap(); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // The reaction is added to the local echo. - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { - let reactions = item.as_event().unwrap().reactions(); - assert_eq!(reactions.len(), 1); - let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); - assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); - }); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); + let reactions = item.as_event().unwrap().reactions(); + assert_eq!(reactions.len(), 1); + let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); + assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); // Add another reaction. let key2 = "😈"; timeline.toggle_reaction(&item_id, key2).await.unwrap(); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // Also comes as a local echo. - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { - let reactions = item.as_event().unwrap().reactions(); - assert_eq!(reactions.len(), 2); - let reaction_info = reactions.get(key2).unwrap().get(user_id).unwrap(); - assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); - }); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); + let reactions = item.as_event().unwrap().reactions(); + assert_eq!(reactions.len(), 2); + let reaction_info = reactions.get(key2).unwrap().get(user_id).unwrap(); + assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); // Remove second reaction. It's immediately removed, since it was a local echo, // and it wasn't being sent. timeline.toggle_reaction(&item_id, key2).await.unwrap(); - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { - let reactions = item.as_event().unwrap().reactions(); - assert_eq!(reactions.len(), 1); - let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); - assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); - }); + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); + let reactions = item.as_event().unwrap().reactions(); + assert_eq!(reactions.len(), 1); + let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); + assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); + + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); // Now, wait for the remote echo for the message itself. - assert_next_matches_with_timeout!(stream, 2000, VectorDiff::Set { index: 1, value: item } => { - let reactions = item.as_event().unwrap().reactions(); - assert_eq!(reactions.len(), 1); - let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); - // TODO(bnjbvr): why not LocalToRemote here? - assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); - }); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); + let reactions = item.as_event().unwrap().reactions(); + assert_eq!(reactions.len(), 1); + let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); + // TODO(bnjbvr): why not LocalToRemote here? + assert_matches!(&reaction_info.status, ReactionStatus::LocalToLocal(..)); + + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); // And then the remote echo for the reaction itself. - assert_next_matches_with_timeout!(stream, VectorDiff::Set { index: 1, value: item } => { - let reactions = item.as_event().unwrap().reactions(); - assert_eq!(reactions.len(), 1); - let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); - assert_matches!(&reaction_info.status, ReactionStatus::RemoteToRemote(..)); - }); + assert_let!(VectorDiff::Set { index: 1, value: item } = &timeline_updates[0]); + let reactions = item.as_event().unwrap().reactions(); + assert_eq!(reactions.len(), 1); + let reaction_info = reactions.get(key1).unwrap().get(user_id).unwrap(); + assert_matches!(&reaction_info.status, ReactionStatus::RemoteToRemote(..)); // And we're done. tokio::time::sleep(Duration::from_millis(150)).await; - assert!(stream.next().now_or_never().is_none()); + assert_pending!(stream); } diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs index 22e39defb..837884892 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/read_receipts.rs @@ -132,8 +132,11 @@ async fn test_read_receipts_updates() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 5); + // We don't list the read receipt of our own user on events. - assert_let!(Some(VectorDiff::PushBack { value: first_item }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: first_item } = &timeline_updates[0]); let first_event = first_item.as_event().unwrap(); assert!(first_event.read_receipts().is_empty()); @@ -144,25 +147,23 @@ async fn test_read_receipts_updates() { assert_pending!(own_receipts_subscriber); // Implicit read receipt of @alice:localhost. - assert_let!(Some(VectorDiff::PushBack { value: second_item }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: second_item } = &timeline_updates[1]); let second_event = second_item.as_event().unwrap(); assert_eq!(second_event.read_receipts().len(), 1); // Read receipt of @alice:localhost is moved to third event. - assert_let!( - Some(VectorDiff::Set { index: 1, value: second_item }) = timeline_stream.next().await - ); + assert_let!(VectorDiff::Set { index: 1, value: second_item } = &timeline_updates[2]); let second_event = second_item.as_event().unwrap(); assert!(second_event.read_receipts().is_empty()); - assert_let!(Some(VectorDiff::PushBack { value: third_item }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: third_item } = &timeline_updates[3]); let third_event = third_item.as_event().unwrap(); assert_eq!(third_event.read_receipts().len(), 1); let (alice_receipt_event_id, _) = timeline.latest_user_read_receipt(alice).await.unwrap(); assert_eq!(alice_receipt_event_id, third_event_id); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[4]); assert!(date_divider.is_date_divider()); // Read receipt on unknown event is ignored. @@ -254,9 +255,10 @@ async fn test_read_receipts_updates() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!( - Some(VectorDiff::Set { index: 3, value: third_item }) = timeline_stream.next().await - ); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + + assert_let!(VectorDiff::Set { index: 3, value: third_item } = &timeline_updates[0]); let third_event = third_item.as_event().unwrap(); assert_eq!(third_event.read_receipts().len(), 2); @@ -291,6 +293,7 @@ async fn test_read_receipts_updates() { assert_ready!(own_receipts_subscriber); assert_pending!(own_receipts_subscriber); + assert_pending!(timeline_stream); } #[async_test] @@ -377,8 +380,11 @@ async fn test_read_receipts_updates_on_filtered_events() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 4); + // We don't list the read receipt of our own user on events. - assert_let!(Some(VectorDiff::PushBack { value: item_a }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: item_a } = &timeline_updates[0]); let event_a = item_a.as_event().unwrap(); assert!(event_a.read_receipts().is_empty()); @@ -389,7 +395,7 @@ async fn test_read_receipts_updates_on_filtered_events() { assert_eq!(own_receipt_timeline_event, event_a_id); // Implicit read receipt of @bob:localhost. - assert_let!(Some(VectorDiff::Set { index: 0, value: item_a }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 0, value: item_a } = &timeline_updates[1]); let event_a = item_a.as_event().unwrap(); assert_eq!(event_a.read_receipts().len(), 1); @@ -402,7 +408,7 @@ async fn test_read_receipts_updates_on_filtered_events() { assert_eq!(bob_receipt_timeline_event, event_a.event_id().unwrap()); // Implicit read receipt of @alice:localhost. - assert_let!(Some(VectorDiff::PushBack { value: item_c }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: item_c } = &timeline_updates[2]); let event_c = item_c.as_event().unwrap(); assert_eq!(event_c.read_receipts().len(), 1); @@ -412,7 +418,7 @@ async fn test_read_receipts_updates_on_filtered_events() { timeline.latest_user_read_receipt_timeline_event_id(*ALICE).await.unwrap(); assert_eq!(alice_receipt_timeline_event, event_c_id); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[3]); assert!(date_divider.is_date_divider()); // Read receipt on filtered event. @@ -463,11 +469,14 @@ async fn test_read_receipts_updates_on_filtered_events() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::Set { index: 1, value: item_a }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::Set { index: 1, value: item_a } = &timeline_updates[0]); let event_a = item_a.as_event().unwrap(); assert!(event_a.read_receipts().is_empty()); - assert_let!(Some(VectorDiff::Set { index: 2, value: item_c }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 2, value: item_c } = &timeline_updates[1]); let event_c = item_c.as_event().unwrap(); assert_eq!(event_c.read_receipts().len(), 2); @@ -505,6 +514,7 @@ async fn test_read_receipts_updates_on_filtered_events() { let own_receipt_timeline_event = timeline.latest_user_read_receipt_timeline_event_id(own_user_id).await.unwrap(); assert_eq!(own_receipt_timeline_event, event_c_id); + assert_pending!(timeline_stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs index 82450c1a8..d00b2ae3e 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs @@ -24,7 +24,7 @@ use ruma::{ owned_event_id, room_id, }; use serde_json::json; -use stream_assert::assert_next_matches; +use stream_assert::{assert_next_matches, assert_pending}; use tokio::task::yield_now; use wiremock::{ matchers::{header, method, path_regex}, @@ -71,17 +71,20 @@ async fn test_in_reply_to_details() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 3); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); assert_matches!(first.as_event().unwrap().content(), TimelineItemContent::Message(_)); - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: second } = &timeline_updates[1]); let second_event = second.as_event().unwrap(); assert_let!(TimelineItemContent::Message(message) = second_event.content()); let in_reply_to = message.in_reply_to().unwrap(); assert_eq!(in_reply_to.event_id, event_id!("$event1")); assert_matches!(in_reply_to.event, TimelineDetails::Ready(_)); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[2]); assert!(date_divider.is_date_divider()); // Add an reply to an unknown event to the timeline @@ -95,11 +98,12 @@ async fn test_in_reply_to_details() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!( - Some(VectorDiff::Set { value: _read_receipt_update, .. }) = timeline_stream.next().await - ); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); - assert_let!(Some(VectorDiff::PushBack { value: third }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { value: _read_receipt_update, .. } = &timeline_updates[0]); + + assert_let!(VectorDiff::PushBack { value: third } = &timeline_updates[1]); let third_event = third.as_event().unwrap(); assert_let!(TimelineItemContent::Message(message) = third_event.content()); let in_reply_to = message.in_reply_to().unwrap(); @@ -124,12 +128,15 @@ async fn test_in_reply_to_details() { timeline.fetch_details_for_event(third_event.event_id().unwrap()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::Set { index: 3, value: third } = &timeline_updates[0]); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Pending); assert_eq!(*third.unique_id(), unique_id); - assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 3, value: third } = &timeline_updates[1]); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Error(_)); assert_eq!(*third.unique_id(), unique_id); @@ -153,15 +160,20 @@ async fn test_in_reply_to_details() { timeline.fetch_details_for_event(third_event.event_id().unwrap()).await.unwrap(); - assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::Set { index: 3, value: third } = &timeline_updates[0]); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Pending); assert_eq!(*third.unique_id(), unique_id); - assert_let!(Some(VectorDiff::Set { index: 3, value: third }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 3, value: third } = &timeline_updates[1]); assert_let!(TimelineItemContent::Message(message) = third.as_event().unwrap().content()); assert_matches!(message.in_reply_to().unwrap().event, TimelineDetails::Ready(_)); assert_eq!(*third.unique_id(), unique_id); + + assert_pending!(timeline_stream); } #[async_test] diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs index f2fdc800d..42cacc4f0 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/sliding_sync.rs @@ -18,7 +18,7 @@ use anyhow::{Context as _, Result}; use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::{Vector, VectorDiff}; -use futures_util::{pin_mut, FutureExt, Stream, StreamExt}; +use futures_util::{pin_mut, Stream, StreamExt}; use matrix_sdk::{ test_utils::logged_in_client_with_server, Client, SlidingSync, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, UpdateSummary, @@ -71,17 +71,17 @@ pub(crate) use timeline_event; macro_rules! assert_timeline_stream { // `--- date divider ---` - ( @_ [ $stream:ident ] [ --- date divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + ( @_ [ $iterator:ident ] [ --- date divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ - [ $stream ] + [ $iterator ] [ $( $rest )* ] [ $( $accumulator )* { assert_matches!( - $stream.next().now_or_never(), - Some(Some(VectorDiff::PushBack { value })) => { + $iterator .next(), + Some(VectorDiff::PushBack { value }) => { assert_matches!( **value, TimelineItemKind::Virtual( @@ -96,17 +96,17 @@ macro_rules! assert_timeline_stream { }; // `append "$event_id"` - ( @_ [ $stream:ident ] [ append $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + ( @_ [ $iterator:ident ] [ append $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ - [ $stream ] + [ $iterator ] [ $( $rest )* ] [ $( $accumulator )* { assert_matches!( - $stream.next().now_or_never(), - Some(Some(VectorDiff::PushBack { value })) => { + $iterator .next(), + Some(VectorDiff::PushBack { value }) => { assert_matches!( &**value, TimelineItemKind::Event(event_timeline_item) => { @@ -121,17 +121,17 @@ macro_rules! assert_timeline_stream { }; // `prepend --- date divider ---` - ( @_ [ $stream:ident ] [ prepend --- date divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + ( @_ [ $iterator:ident ] [ prepend --- date divider --- ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ - [ $stream ] + [ $iterator ] [ $( $rest )* ] [ $( $accumulator )* { assert_matches!( - $stream.next().now_or_never(), - Some(Some(VectorDiff::PushFront { value })) => { + $iterator .next(), + Some(VectorDiff::PushFront { value }) => { assert_matches!( &**value, TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(_)) => {} @@ -145,17 +145,17 @@ macro_rules! assert_timeline_stream { // `insert [$nth] "$event_id"` - ( @_ [ $stream:ident ] [ insert [$index:literal] $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + ( @_ [ $iterator:ident ] [ insert [$index:literal] $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ - [ $stream ] + [ $iterator ] [ $( $rest )* ] [ $( $accumulator )* { assert_matches!( - $stream.next().now_or_never(), - Some(Some(VectorDiff::Insert { index: $index, value })) => { + $iterator .next(), + Some(VectorDiff::Insert { index: $index, value }) => { assert_matches!( &**value, TimelineItemKind::Event(event_timeline_item) => { @@ -170,17 +170,17 @@ macro_rules! assert_timeline_stream { }; // `update [$nth] "$event_id"` - ( @_ [ $stream:ident ] [ update [$index:literal] $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + ( @_ [ $iterator:ident ] [ update [$index:literal] $event_id:literal ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ - [ $stream ] + [ $iterator ] [ $( $rest )* ] [ $( $accumulator )* { assert_matches!( - $stream.next().now_or_never(), - Some(Some(VectorDiff::Set { index: $index, value })) => { + $iterator .next(), + Some(VectorDiff::Set { index: $index, value }) => { assert_matches!( &**value, TimelineItemKind::Event(event_timeline_item) => { @@ -195,17 +195,17 @@ macro_rules! assert_timeline_stream { }; // `remove [$nth]` - ( @_ [ $stream:ident ] [ remove [$index:literal] ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { + ( @_ [ $iterator:ident ] [ remove [$index:literal] ; $( $rest:tt )* ] [ $( $accumulator:tt )* ] ) => { assert_timeline_stream!( @_ - [ $stream ] + [ $iterator ] [ $( $rest )* ] [ $( $accumulator )* { assert_matches!( - $stream.next().now_or_never(), - Some(Some(VectorDiff::Remove { index: $index })) + $iterator .next(), + Some(VectorDiff::Remove { index: $index }) ); } ] @@ -217,7 +217,13 @@ macro_rules! assert_timeline_stream { }; ( [ $stream:ident ] $( $all:tt )* ) => { - assert_timeline_stream!( @_ [ $stream ] [ $( $all )* ] [] ) + let mut timeline_updates = $stream + .next() + .await + .expect("Failed to poll the stream") + .into_iter(); + + assert_timeline_stream!( @_ [ timeline_updates ] [ $( $all )* ] [] ) }; } @@ -273,7 +279,7 @@ async fn timeline_test_helper( client: &Client, sliding_sync: &SlidingSync, room_id: &RoomId, -) -> Result<(Vector>, impl Stream>>)> { +) -> Result<(Vector>, impl Stream>>>)> { let sliding_sync_room = sliding_sync.get_room(room_id).await.unwrap(); let room_id = sliding_sync_room.room_id(); @@ -498,11 +504,12 @@ async fn test_timeline_read_receipts_are_updated_live() -> Result<()> { } }; - assert_let!( - Some(Some(VectorDiff::Set { index: 2, value })) = timeline_stream.next().now_or_never() - ); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); - assert_let!(TimelineItemKind::Event(event_timeline_item) = &**value); + assert_let!(VectorDiff::Set { index: 2, value } = &timeline_updates[0]); + + assert_let!(TimelineItemKind::Event(event_timeline_item) = &***value); assert_eq!(event_timeline_item.event_id().unwrap().as_str(), "$x2:bar.org"); let read_receipts = event_timeline_item.read_receipts(); diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs index 63ae21313..b6c57f82e 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/subscribe.rs @@ -17,7 +17,7 @@ use std::time::Duration; use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; -use futures_util::{pin_mut, StreamExt}; +use futures_util::StreamExt; use matrix_sdk::{config::SyncSettings, test_utils::logged_in_client_with_server}; use matrix_sdk_test::{ async_test, event_factory::EventFactory, mocks::mock_encryption_state, sync_timeline_event, @@ -30,7 +30,7 @@ use ruma::{ room_id, user_id, }; use serde_json::json; -use stream_assert::{assert_next_matches, assert_pending}; +use stream_assert::assert_pending; use crate::mock_sync; @@ -52,7 +52,7 @@ async fn test_batched() { let room = client.get_room(room_id).unwrap(); let timeline = room.timeline_builder().event_filter(|_, _| true).build().await.unwrap(); - let (_, mut timeline_stream) = timeline.subscribe_batched().await; + let (_, mut timeline_stream) = timeline.subscribe().await; let hdl = tokio::spawn(async move { let next_batch = timeline_stream.next().await.unwrap(); @@ -113,7 +113,10 @@ async fn test_event_filter() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: first }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 2); + + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); let first_event = first.as_event().unwrap(); assert_eq!(first_event.event_id(), Some(first_event_id)); assert_eq!(first_event.read_receipts().len(), 1, "implicit read receipt"); @@ -122,7 +125,7 @@ async fn test_event_filter() { assert_matches!(msg.msgtype(), MessageType::Text(_)); assert!(!msg.is_edited()); - assert_let!(Some(VectorDiff::PushFront { value: date_divider }) = timeline_stream.next().await); + assert_let!(VectorDiff::PushFront { value: date_divider } = &timeline_updates[1]); assert!(date_divider.is_date_divider()); let second_event_id = event_id!("$Ga6Y2l0gKY"); @@ -165,18 +168,21 @@ async fn test_event_filter() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_let!(Some(VectorDiff::PushBack { value: second }) = timeline_stream.next().await); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 3); + + assert_let!(VectorDiff::PushBack { value: second } = &timeline_updates[0]); let second_event = second.as_event().unwrap(); assert_eq!(second_event.event_id(), Some(second_event_id)); assert_eq!(second_event.read_receipts().len(), 1, "implicit read receipt"); // The implicit read receipt of Alice is moving from Alice's message... - assert_let!(Some(VectorDiff::Set { index: 1, value: first }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: first } = &timeline_updates[1]); assert_eq!(first.as_event().unwrap().read_receipts().len(), 0, "no more implicit read receipt"); // … to Alice's edit. But since this item isn't visible, it's lost in the weeds! // The edit is applied to the first event. - assert_let!(Some(VectorDiff::Set { index: 1, value: first }) = timeline_stream.next().await); + assert_let!(VectorDiff::Set { index: 1, value: first } = &timeline_updates[2]); let first_event = first.as_event().unwrap(); assert!(first_event.read_receipts().is_empty()); assert_matches!(first_event.latest_edit_json(), Some(_)); @@ -184,6 +190,8 @@ async fn test_event_filter() { assert_let!(MessageType::Text(text) = msg.msgtype()); assert_eq!(text.body, "hi"); assert!(msg.is_edited()); + + assert_pending!(timeline_stream); } #[async_test] @@ -203,8 +211,7 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { let room = client.get_room(room_id).unwrap(); let timeline = room.timeline_builder().build().await.unwrap(); - let (_, timeline_stream) = timeline.subscribe().await; - pin_mut!(timeline_stream); + let (_, mut timeline_stream) = timeline.subscribe().await; let alice = user_id!("@alice:example.org"); let bob = user_id!("@bob:example.org"); @@ -228,21 +235,24 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(first_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(second_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(first_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(third_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 5); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[0]); + assert_eq!(value.as_event().unwrap().event_id(), Some(first_event_id)); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + assert_eq!(value.as_event().unwrap().event_id(), Some(second_event_id)); + + assert_let!(VectorDiff::Set { index: 0, value } = &timeline_updates[2]); + assert_eq!(value.as_event().unwrap().event_id(), Some(first_event_id)); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[3]); + assert_eq!(value.as_event().unwrap().event_id(), Some(third_event_id)); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[4]); + assert!(value.is_date_divider()); + assert_pending!(timeline_stream); sync_builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom(json!({ @@ -258,9 +268,11 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // The timeline has been emptied. - assert_next_matches!(timeline_stream, VectorDiff::Clear); - assert_pending!(timeline_stream); + assert_let!(VectorDiff::Clear = &timeline_updates[0]); let fourth_event_id = event_id!("$YTQwYl2pl4"); let fifth_event_id = event_id!("$YTQwYl2pl5"); @@ -278,21 +290,25 @@ async fn test_timeline_is_reset_when_a_user_is_ignored_or_unignored() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 5); + // Timeline receives events as before. - assert_next_matches!(timeline_stream, VectorDiff::Clear); // TODO: Remove `RoomEventCacheUpdate::Clear` as it creates double - // `VectorDiff::Clear`. - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(fourth_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 0, value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(fourth_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(fifth_event_id)); - }); - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::Clear = &timeline_updates[0]); // TODO: Remove `RoomEventCacheUpdate::Clear` as it creates double + // `VectorDiff::Clear`. + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]); + assert_eq!(value.as_event().unwrap().event_id(), Some(fourth_event_id)); + + assert_let!(VectorDiff::Set { index: 0, value } = &timeline_updates[2]); + assert_eq!(value.as_event().unwrap().event_id(), Some(fourth_event_id)); + + assert_let!(VectorDiff::PushBack { value } = &timeline_updates[3]); + assert_eq!(value.as_event().unwrap().event_id(), Some(fifth_event_id)); + + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[4]); + assert!(value.is_date_divider()); + assert_pending!(timeline_stream); } @@ -313,8 +329,7 @@ async fn test_profile_updates() { let room = client.get_room(room_id).unwrap(); let timeline = room.timeline_builder().build().await.unwrap(); - let (_, timeline_stream) = timeline.subscribe().await; - pin_mut!(timeline_stream); + let (_, mut timeline_stream) = timeline.subscribe().await; let alice = "@alice:example.org"; let bob = "@bob:example.org"; @@ -351,19 +366,21 @@ async fn test_profile_updates() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - let item_1 = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 3); + + assert_let!(VectorDiff::PushBack { value: item_1 } = &timeline_updates[0]); let event_1_item = item_1.as_event().unwrap(); assert_eq!(event_1_item.event_id(), Some(event_1_id)); assert_matches!(event_1_item.sender_profile(), TimelineDetails::Unavailable); - let item_2 = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(VectorDiff::PushBack { value: item_2 } = &timeline_updates[1]); let event_2_item = item_2.as_event().unwrap(); assert_eq!(event_2_item.event_id(), Some(event_2_id)); assert_matches!(event_2_item.sender_profile(), TimelineDetails::Unavailable); - assert_next_matches!(timeline_stream, VectorDiff::PushFront { value } => { - assert!(value.is_date_divider()); - }); + assert_let!(VectorDiff::PushFront { value } = &timeline_updates[2]); + assert!(value.is_date_divider()); assert_pending!(timeline_stream); @@ -412,13 +429,15 @@ async fn test_profile_updates() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 8); + // Read receipt change. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(event_2_id)); - }); + assert_let!(VectorDiff::Set { index: 2, value } = &timeline_updates[0]); + assert_eq!(value.as_event().unwrap().event_id(), Some(event_2_id)); // The events are added. - let item_3 = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(VectorDiff::PushBack { value: item_3 } = &timeline_updates[1]); let event_3_item = item_3.as_event().unwrap(); assert_eq!(event_3_item.event_id(), Some(event_3_id)); let profile = @@ -427,11 +446,10 @@ async fn test_profile_updates() { assert!(!profile.display_name_ambiguous); // Read receipt change. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(event_1_id)); - }); + assert_let!(VectorDiff::Set { index: 1, value } = &timeline_updates[2]); + assert_eq!(value.as_event().unwrap().event_id(), Some(event_1_id)); - let item_4 = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(VectorDiff::PushBack { value: item_4 } = &timeline_updates[3]); let event_4_item = item_4.as_event().unwrap(); assert_eq!(event_4_item.event_id(), Some(event_4_id)); let profile = @@ -440,11 +458,10 @@ async fn test_profile_updates() { assert!(!profile.display_name_ambiguous); // Read receipt change. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 4, value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(event_4_id)); - }); + assert_let!(VectorDiff::Set { index: 4, value } = &timeline_updates[4]); + assert_eq!(value.as_event().unwrap().event_id(), Some(event_4_id)); - let item_5 = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(VectorDiff::PushBack { value: item_5 } = &timeline_updates[5]); let event_5_item = item_5.as_event().unwrap(); assert_eq!(event_5_item.event_id(), Some(event_5_id)); let profile = @@ -453,8 +470,7 @@ async fn test_profile_updates() { assert!(!profile.display_name_ambiguous); // The profiles changed. - let item_1 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => value); + assert_let!(VectorDiff::Set { index: 1, value: item_1 } = &timeline_updates[6]); let event_1_item = item_1.as_event().unwrap(); assert_eq!(event_1_item.event_id(), Some(event_1_id)); let profile = @@ -462,8 +478,7 @@ async fn test_profile_updates() { assert_eq!(profile.display_name.as_deref(), Some("Alice")); assert!(!profile.display_name_ambiguous); - let item_2 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => value); + assert_let!(VectorDiff::Set { index: 2, value: item_2 } = &timeline_updates[7]); let event_2_item = item_2.as_event().unwrap(); assert_eq!(event_2_item.event_id(), Some(event_2_id)); let profile = @@ -471,8 +486,6 @@ async fn test_profile_updates() { assert_eq!(profile.display_name.as_deref(), Some("Member")); assert!(!profile.display_name_ambiguous); - assert_pending!(timeline_stream); - // Change name to be ambiguous. let event_6_id = event_id!("$YTQwYl2pl6"); @@ -494,13 +507,15 @@ async fn test_profile_updates() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_eq!(timeline_updates.len(), 7); + // Read receipt change. - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 5, value } => { - assert_eq!(value.as_event().unwrap().event_id(), Some(event_5_id)); - }); + assert_let!(VectorDiff::Set { index: 5, value } = &timeline_updates[0]); + assert_eq!(value.as_event().unwrap().event_id(), Some(event_5_id)); // The event is added. - let item_6 = assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_let!(VectorDiff::PushBack { value: item_6 } = &timeline_updates[1]); let event_6_item = item_6.as_event().unwrap(); assert_eq!(event_6_item.event_id(), Some(event_6_id)); let profile = @@ -509,8 +524,7 @@ async fn test_profile_updates() { assert!(profile.display_name_ambiguous); // The profiles changed. - let item_1 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 1, value } => value); + assert_let!(VectorDiff::Set { index: 1, value: item_1 } = &timeline_updates[2]); let event_1_item = item_1.as_event().unwrap(); assert_eq!(event_1_item.event_id(), Some(event_1_id)); let profile = @@ -518,8 +532,7 @@ async fn test_profile_updates() { assert_eq!(profile.display_name.as_deref(), Some("Member")); assert!(profile.display_name_ambiguous); - let item_2 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 2, value } => value); + assert_let!(VectorDiff::Set { index: 2, value: item_2 } = &timeline_updates[3]); let event_2_item = item_2.as_event().unwrap(); assert_eq!(event_2_item.event_id(), Some(event_2_id)); let profile = @@ -527,8 +540,7 @@ async fn test_profile_updates() { assert_eq!(profile.display_name.as_deref(), Some("Member")); assert!(profile.display_name_ambiguous); - let item_3 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 3, value } => value); + assert_let!(VectorDiff::Set { index: 3, value: item_3 } = &timeline_updates[4]); let event_3_item = item_3.as_event().unwrap(); assert_eq!(event_3_item.event_id(), Some(event_3_id)); let profile = @@ -536,8 +548,7 @@ async fn test_profile_updates() { assert_eq!(profile.display_name.as_deref(), Some("Member")); assert!(profile.display_name_ambiguous); - let item_4 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 4, value } => value); + assert_let!(VectorDiff::Set { index: 4, value: item_4 } = &timeline_updates[5]); let event_4_item = item_4.as_event().unwrap(); assert_eq!(event_4_item.event_id(), Some(event_4_id)); let profile = @@ -545,8 +556,7 @@ async fn test_profile_updates() { assert_eq!(profile.display_name.as_deref(), Some("Member")); assert!(profile.display_name_ambiguous); - let item_5 = - assert_next_matches!(timeline_stream, VectorDiff::Set { index: 5, value } => value); + assert_let!(VectorDiff::Set { index: 5, value: item_5 } = &timeline_updates[6]); let event_5_item = item_5.as_event().unwrap(); assert_eq!(event_5_item.event_id(), Some(event_5_id)); let profile = diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 949df3de4..55c64be6f 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -33,6 +33,11 @@ All notable changes to this project will be documented in this file. ### Refactor +- [**breaking**]: The reexported types `SyncTimelineEvent` and `TimelineEvent` have been fused into a single type + `TimelineEvent`, and its field `push_actions` has been made `Option`al (it is set to `None` when + we couldn't compute the push actions, because we lacked some information). + ([#4568](https://github.com/matrix-org/matrix-rust-sdk/pull/4568)) + - [**breaking**] Move the optional `RequestConfig` argument of the `Client::send()` method to the `with_request_config()` builder method. You should call `Client::send(request).with_request_config(request_config).await` @@ -51,6 +56,12 @@ All notable changes to this project will be documented in this file. - [**breaking**] `Recovery::are_we_the_last_man_standing()` has been renamed to `is_last_device()`. ([#4522](https://github.com/matrix-org/matrix-rust-sdk/pull/4522)) +- [**breaking**] The `matrix_auth` module is now at `authentication::matrix`. + ([#4575](https://github.com/matrix-org/matrix-rust-sdk/pull/4575)) + +- [**breaking**] The `oidc` module is now at `authentication::oidc`. + ([#4575](https://github.com/matrix-org/matrix-rust-sdk/pull/4575)) + ## [0.9.0] - 2024-12-18 ### Bug Fixes diff --git a/crates/matrix-sdk/src/matrix_auth/login_builder.rs b/crates/matrix-sdk/src/authentication/matrix/login_builder.rs similarity index 100% rename from crates/matrix-sdk/src/matrix_auth/login_builder.rs rename to crates/matrix-sdk/src/authentication/matrix/login_builder.rs diff --git a/crates/matrix-sdk/src/matrix_auth/mod.rs b/crates/matrix-sdk/src/authentication/matrix/mod.rs similarity index 99% rename from crates/matrix-sdk/src/matrix_auth/mod.rs rename to crates/matrix-sdk/src/authentication/matrix/mod.rs index 8beb76de1..dfadd9ad0 100644 --- a/crates/matrix-sdk/src/matrix_auth/mod.rs +++ b/crates/matrix-sdk/src/authentication/matrix/mod.rs @@ -760,7 +760,7 @@ impl MatrixAuth { /// ```no_run /// use futures_util::StreamExt; /// use matrix_sdk::Client; - /// # fn persist_session(_: &matrix_sdk::matrix_auth::MatrixSession) {}; + /// # fn persist_session(_: &matrix_sdk::authentication::matrix::MatrixSession) {}; /// # async { /// let homeserver = "http://example.com"; /// let client = Client::builder() @@ -835,7 +835,7 @@ impl MatrixAuth { /// /// ```no_run /// use matrix_sdk::{ - /// matrix_auth::{MatrixSession, MatrixSessionTokens}, + /// authentication::matrix::{MatrixSession, MatrixSessionTokens}, /// ruma::{device_id, user_id}, /// Client, SessionMeta, /// }; @@ -881,7 +881,7 @@ impl MatrixAuth { /// ``` /// /// [`login`]: #method.login - /// [`LoginBuilder::send()`]: crate::matrix_auth::LoginBuilder::send + /// [`LoginBuilder::send()`]: crate::authentication::matrix::LoginBuilder::send #[instrument(skip_all)] pub async fn restore_session(&self, session: MatrixSession) -> Result<()> { debug!("Restoring Matrix auth session"); @@ -959,7 +959,7 @@ impl MatrixAuth { /// /// ``` /// use matrix_sdk::{ -/// matrix_auth::{MatrixSession, MatrixSessionTokens}, +/// authentication::matrix::{MatrixSession, MatrixSessionTokens}, /// SessionMeta, /// }; /// use ruma::{device_id, user_id}; diff --git a/crates/matrix-sdk/src/authentication/mod.rs b/crates/matrix-sdk/src/authentication/mod.rs index 408de8680..1a6fad939 100644 --- a/crates/matrix-sdk/src/authentication/mod.rs +++ b/crates/matrix-sdk/src/authentication/mod.rs @@ -14,21 +14,20 @@ //! Types and functions related to authentication in Matrix. -// TODO:(pixlwave) Move AuthenticationService from the FFI into this module. -// TODO:(poljar) Move the oidc and matrix_auth modules under this module. - use std::sync::Arc; use as_variant::as_variant; use matrix_sdk_base::SessionMeta; use tokio::sync::{broadcast, Mutex, OnceCell}; +pub mod matrix; #[cfg(feature = "experimental-oidc")] -use crate::oidc::{self, Oidc, OidcAuthData, OidcCtx}; -use crate::{ - matrix_auth::{self, MatrixAuth, MatrixAuthData}, - Client, RefreshTokenError, SessionChange, -}; +pub mod oidc; + +use self::matrix::{MatrixAuth, MatrixAuthData}; +#[cfg(feature = "experimental-oidc")] +use self::oidc::{Oidc, OidcAuthData, OidcCtx}; +use crate::{Client, RefreshTokenError, SessionChange}; #[cfg(all(feature = "experimental-oidc", feature = "e2e-encryption", not(target_arch = "wasm32")))] pub mod qrcode; @@ -36,8 +35,8 @@ pub mod qrcode; /// Session tokens, for any kind of authentication. #[allow(missing_debug_implementations, clippy::large_enum_variant)] pub enum SessionTokens { - /// Tokens for a [`matrix_auth`] session. - Matrix(matrix_auth::MatrixSessionTokens), + /// Tokens for a [`matrix`] session. + Matrix(matrix::MatrixSessionTokens), #[cfg(feature = "experimental-oidc")] /// Tokens for an [`oidc`] session. Oidc(oidc::OidcSessionTokens), @@ -103,7 +102,7 @@ pub enum AuthApi { #[non_exhaustive] pub enum AuthSession { /// A session using the native Matrix authentication API. - Matrix(matrix_auth::MatrixSession), + Matrix(matrix::MatrixSession), /// A session using the OpenID Connect API. #[cfg(feature = "experimental-oidc")] @@ -148,8 +147,8 @@ impl AuthSession { } } -impl From for AuthSession { - fn from(session: matrix_auth::MatrixSession) -> Self { +impl From for AuthSession { + fn from(session: matrix::MatrixSession) -> Self { Self::Matrix(session) } } diff --git a/crates/matrix-sdk/src/oidc/auth_code_builder.rs b/crates/matrix-sdk/src/authentication/oidc/auth_code_builder.rs similarity index 100% rename from crates/matrix-sdk/src/oidc/auth_code_builder.rs rename to crates/matrix-sdk/src/authentication/oidc/auth_code_builder.rs diff --git a/crates/matrix-sdk/src/oidc/backend/mock.rs b/crates/matrix-sdk/src/authentication/oidc/backend/mock.rs similarity index 99% rename from crates/matrix-sdk/src/oidc/backend/mock.rs rename to crates/matrix-sdk/src/authentication/oidc/backend/mock.rs index de3573967..e6e67f0ae 100644 --- a/crates/matrix-sdk/src/oidc/backend/mock.rs +++ b/crates/matrix-sdk/src/authentication/oidc/backend/mock.rs @@ -37,7 +37,7 @@ use mas_oidc_client::{ use url::Url; use super::{OidcBackend, OidcError, RefreshedSessionTokens}; -use crate::oidc::{AuthorizationCode, OidcSessionTokens}; +use crate::authentication::oidc::{AuthorizationCode, OidcSessionTokens}; pub(crate) const ISSUER_URL: &str = "https://oidc.example.com/issuer"; pub(crate) const AUTHORIZATION_URL: &str = "https://oidc.example.com/authorization"; diff --git a/crates/matrix-sdk/src/oidc/backend/mod.rs b/crates/matrix-sdk/src/authentication/oidc/backend/mod.rs similarity index 100% rename from crates/matrix-sdk/src/oidc/backend/mod.rs rename to crates/matrix-sdk/src/authentication/oidc/backend/mod.rs diff --git a/crates/matrix-sdk/src/oidc/backend/server.rs b/crates/matrix-sdk/src/authentication/oidc/backend/server.rs similarity index 98% rename from crates/matrix-sdk/src/oidc/backend/server.rs rename to crates/matrix-sdk/src/authentication/oidc/backend/server.rs index b2ea157aa..a97efc20d 100644 --- a/crates/matrix-sdk/src/oidc/backend/server.rs +++ b/crates/matrix-sdk/src/authentication/oidc/backend/server.rs @@ -42,7 +42,7 @@ use url::Url; use super::{OidcBackend, OidcError, RefreshedSessionTokens}; use crate::{ - oidc::{rng, AuthorizationCode, OidcSessionTokens}, + authentication::oidc::{rng, AuthorizationCode, OidcSessionTokens}, Client, }; diff --git a/crates/matrix-sdk/src/oidc/cross_process.rs b/crates/matrix-sdk/src/authentication/oidc/cross_process.rs similarity index 99% rename from crates/matrix-sdk/src/oidc/cross_process.rs rename to crates/matrix-sdk/src/authentication/oidc/cross_process.rs index 87c057d17..3a41f3e0a 100644 --- a/crates/matrix-sdk/src/oidc/cross_process.rs +++ b/crates/matrix-sdk/src/authentication/oidc/cross_process.rs @@ -264,7 +264,7 @@ mod tests { use super::compute_session_hash; use crate::{ - oidc::{ + authentication::oidc::{ backend::mock::{MockImpl, ISSUER_URL}, cross_process::SessionHash, tests, diff --git a/crates/matrix-sdk/src/oidc/data_serde.rs b/crates/matrix-sdk/src/authentication/oidc/data_serde.rs similarity index 100% rename from crates/matrix-sdk/src/oidc/data_serde.rs rename to crates/matrix-sdk/src/authentication/oidc/data_serde.rs diff --git a/crates/matrix-sdk/src/oidc/end_session_builder.rs b/crates/matrix-sdk/src/authentication/oidc/end_session_builder.rs similarity index 100% rename from crates/matrix-sdk/src/oidc/end_session_builder.rs rename to crates/matrix-sdk/src/authentication/oidc/end_session_builder.rs diff --git a/crates/matrix-sdk/src/oidc/mod.rs b/crates/matrix-sdk/src/authentication/oidc/mod.rs similarity index 99% rename from crates/matrix-sdk/src/oidc/mod.rs rename to crates/matrix-sdk/src/authentication/oidc/mod.rs index 5fb525d75..db42bd95a 100644 --- a/crates/matrix-sdk/src/oidc/mod.rs +++ b/crates/matrix-sdk/src/authentication/oidc/mod.rs @@ -217,11 +217,11 @@ pub use self::{ use self::{ backend::{server::OidcServer, OidcBackend}, cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager}, + registrations::{ClientId, OidcRegistrations}, }; use crate::{ authentication::{qrcode::LoginWithQrCode, AuthData}, client::SessionChange, - oidc::registrations::{ClientId, OidcRegistrations}, Client, HttpError, RefreshTokenError, Result, }; diff --git a/crates/matrix-sdk/src/oidc/registrations.rs b/crates/matrix-sdk/src/authentication/oidc/registrations.rs similarity index 100% rename from crates/matrix-sdk/src/oidc/registrations.rs rename to crates/matrix-sdk/src/authentication/oidc/registrations.rs diff --git a/crates/matrix-sdk/src/oidc/tests.rs b/crates/matrix-sdk/src/authentication/oidc/tests.rs similarity index 99% rename from crates/matrix-sdk/src/oidc/tests.rs rename to crates/matrix-sdk/src/authentication/oidc/tests.rs index 0c40cff89..2a01657fc 100644 --- a/crates/matrix-sdk/src/oidc/tests.rs +++ b/crates/matrix-sdk/src/authentication/oidc/tests.rs @@ -26,14 +26,11 @@ use wiremock::{ use super::{ backend::mock::{MockImpl, AUTHORIZATION_URL, ISSUER_URL}, + registrations::{ClientId, OidcRegistrations}, AuthorizationCode, AuthorizationError, AuthorizationResponse, Oidc, OidcError, OidcSession, OidcSessionTokens, RedirectUriQueryParseError, UserSession, }; -use crate::{ - oidc::registrations::{ClientId, OidcRegistrations}, - test_utils::test_client_builder, - Client, Error, -}; +use crate::{test_utils::test_client_builder, Client, Error}; const CLIENT_ID: &str = "test_client_id"; const REDIRECT_URI_STRING: &str = "http://matrix.example.com/oidc/callback"; diff --git a/crates/matrix-sdk/src/authentication/qrcode/login.rs b/crates/matrix-sdk/src/authentication/qrcode/login.rs index 4422d6b07..9bf13daaa 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/login.rs @@ -34,7 +34,7 @@ use super::{ SecureChannelError, }; #[cfg(doc)] -use crate::oidc::Oidc; +use crate::authentication::oidc::Oidc; use crate::{ authentication::qrcode::{ messages::QrAuthMessage, secure_channel::EstablishedSecureChannel, QRCodeLoginError, diff --git a/crates/matrix-sdk/src/authentication/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/qrcode/mod.rs index 752f21e0c..0044f36a6 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/mod.rs @@ -33,8 +33,8 @@ use url::Url; pub use vodozemac::ecies::{Error as EciesError, MessageDecodeError}; #[cfg(doc)] -use crate::oidc::Oidc; -use crate::{oidc::CrossProcessRefreshLockError, HttpError}; +use crate::authentication::oidc::Oidc; +use crate::{authentication::oidc::CrossProcessRefreshLockError, HttpError}; mod login; mod messages; @@ -113,7 +113,7 @@ pub enum DeviceAuhorizationOidcError { /// A generic OIDC error happened while we were attempting to register the /// device with the OIDC provider. #[error(transparent)] - Oidc(#[from] crate::oidc::OidcError), + Oidc(#[from] crate::authentication::oidc::OidcError), /// The issuer URL failed to be parsed. #[error(transparent)] diff --git a/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs b/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs index 3cdc3a0a2..1adce855a 100644 --- a/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs +++ b/crates/matrix-sdk/src/authentication/qrcode/oidc_client.rs @@ -32,7 +32,7 @@ use openidconnect::{ use vodozemac::Curve25519PublicKey; use super::DeviceAuhorizationOidcError; -use crate::{http_client::HttpClient, oidc::OidcSessionTokens}; +use crate::{authentication::oidc::OidcSessionTokens, http_client::HttpClient}; // Obtain the device_authorization_url from the OIDC metadata provider. #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/matrix-sdk/src/client/builder/mod.rs b/crates/matrix-sdk/src/client/builder/mod.rs index 8902f2360..bf4ab18ca 100644 --- a/crates/matrix-sdk/src/client/builder/mod.rs +++ b/crates/matrix-sdk/src/client/builder/mod.rs @@ -28,14 +28,14 @@ use tokio::sync::{broadcast, Mutex, OnceCell}; use tracing::{debug, field::debug, instrument, Span}; use super::{Client, ClientInner}; +#[cfg(feature = "experimental-oidc")] +use crate::authentication::oidc::OidcCtx; #[cfg(feature = "e2e-encryption")] use crate::crypto::{CollectStrategy, TrustRequirement}; #[cfg(feature = "e2e-encryption")] use crate::encryption::EncryptionSettings; #[cfg(not(target_arch = "wasm32"))] use crate::http_client::HttpSettings; -#[cfg(feature = "experimental-oidc")] -use crate::oidc::OidcCtx; use crate::{ authentication::AuthCtx, client::ClientServerCapabilities, config::RequestConfig, error::RumaApiError, http_client::HttpClient, send_queue::SendQueueData, diff --git a/crates/matrix-sdk/src/client/futures.rs b/crates/matrix-sdk/src/client/futures.rs index 7c828cc7b..4cd5ebce1 100644 --- a/crates/matrix-sdk/src/client/futures.rs +++ b/crates/matrix-sdk/src/client/futures.rs @@ -35,7 +35,7 @@ use tracing::trace; use super::super::Client; #[cfg(feature = "experimental-oidc")] -use crate::oidc::OidcError; +use crate::authentication::oidc::OidcError; use crate::{ config::RequestConfig, error::{HttpError, HttpResult}, diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index f2daad94a..27d6ece52 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -74,9 +74,11 @@ use url::Url; use self::futures::SendRequest; #[cfg(feature = "experimental-oidc")] -use crate::oidc::Oidc; +use crate::authentication::oidc::Oidc; use crate::{ - authentication::{AuthCtx, AuthData, ReloadSessionCallback, SaveSessionCallback}, + authentication::{ + matrix::MatrixAuth, AuthCtx, AuthData, ReloadSessionCallback, SaveSessionCallback, + }, config::RequestConfig, deduplicating_handler::DeduplicatingHandler, error::{HttpError, HttpResult}, @@ -86,7 +88,6 @@ use crate::{ EventHandlerStore, ObservableEventHandler, SyncEvent, }, http_client::HttpClient, - matrix_auth::MatrixAuth, notification_settings::NotificationSettings, room_preview::RoomPreview, send_queue::SendQueueData, diff --git a/crates/matrix-sdk/src/docs/encryption.md b/crates/matrix-sdk/src/docs/encryption.md index 0858260e6..91aa1df6e 100644 --- a/crates/matrix-sdk/src/docs/encryption.md +++ b/crates/matrix-sdk/src/docs/encryption.md @@ -233,4 +233,4 @@ is **not** supported using the default store. [`StoreConfig`]: crate::config::StoreConfig [`ClientBuilder`]: crate::ClientBuilder [`ClientBuilder::store_config`]: crate::ClientBuilder::store_config -[`MatrixAuth::login_username()`]: crate::matrix_auth::MatrixAuth::login_username +[`MatrixAuth::login_username()`]: crate::authentication::matrix::MatrixAuth::login_username diff --git a/crates/matrix-sdk/src/encryption/backups/mod.rs b/crates/matrix-sdk/src/encryption/backups/mod.rs index 538bc238c..d48ec2f1f 100644 --- a/crates/matrix-sdk/src/encryption/backups/mod.rs +++ b/crates/matrix-sdk/src/encryption/backups/mod.rs @@ -692,12 +692,9 @@ impl Backups { .upload_progress .set(UploadState::Uploading(new_counts)); - #[cfg(not(target_arch = "wasm32"))] - { - let delay = - self.client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned(); - tokio::time::sleep(delay).await; - } + let delay = + self.client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned(); + crate::sleep::sleep(delay).await; Ok(()) } diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index e59bf74ab..efad584e9 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -1778,9 +1778,9 @@ mod tests { use crate::{ assert_next_matches_with_timeout, + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, encryption::VerificationState, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{logged_in_client, no_retry_test_client, set_client_session}, Client, }; diff --git a/crates/matrix-sdk/src/encryption/tasks.rs b/crates/matrix-sdk/src/encryption/tasks.rs index 5955a1ea9..55c67d386 100644 --- a/crates/matrix-sdk/src/encryption/tasks.rs +++ b/crates/matrix-sdk/src/encryption/tasks.rs @@ -216,7 +216,7 @@ impl BackupDownloadTask { ) { // Wait a bit, perhaps the room key will arrive in the meantime. #[cfg(not(test))] - tokio::time::sleep(Duration::from_millis(Self::DOWNLOAD_DELAY_MILLIS)).await; + crate::sleep::sleep(Duration::from_millis(Self::DOWNLOAD_DELAY_MILLIS)).await; // Now take the lock, and check that we still want to do a download. If we do, // keep hold of a strong reference to the `Client`. diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 99658d2fc..241ebab8f 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -354,7 +354,7 @@ pub enum Error { /// An error occurred interacting with the OpenID Connect API. #[cfg(feature = "experimental-oidc")] #[error(transparent)] - Oidc(#[from] crate::oidc::OidcError), + Oidc(#[from] crate::authentication::oidc::OidcError), /// A concurrent request to a deduplicated request has failed. #[error("a concurrent request failed; see logs for details")] @@ -561,7 +561,7 @@ pub enum RefreshTokenError { /// An error occurred interacting with the OpenID Connect API. #[cfg(feature = "experimental-oidc")] #[error(transparent)] - Oidc(#[from] Arc), + Oidc(#[from] Arc), } /// Errors that can occur when manipulating push notification settings. diff --git a/crates/matrix-sdk/src/event_cache/deduplicator.rs b/crates/matrix-sdk/src/event_cache/deduplicator.rs index 7dbd54eec..d84807f37 100644 --- a/crates/matrix-sdk/src/event_cache/deduplicator.rs +++ b/crates/matrix-sdk/src/event_cache/deduplicator.rs @@ -155,18 +155,18 @@ pub enum Decoration { #[cfg(test)] mod tests { use assert_matches2::{assert_let, assert_matches}; - use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_base::deserialized_responses::TimelineEvent; use matrix_sdk_test::event_factory::EventFactory; use ruma::{owned_event_id, user_id, EventId}; use super::*; - fn sync_timeline_event(event_id: &EventId) -> SyncTimelineEvent { + fn sync_timeline_event(event_id: &EventId) -> TimelineEvent { EventFactory::new() .text_msg("") .sender(user_id!("@mnt_io:matrix.org")) .event_id(event_id) - .into_sync() + .into_event() } #[test] diff --git a/crates/matrix-sdk/src/event_cache/mod.rs b/crates/matrix-sdk/src/event_cache/mod.rs index 0ea2fa05b..4c4622229 100644 --- a/crates/matrix-sdk/src/event_cache/mod.rs +++ b/crates/matrix-sdk/src/event_cache/mod.rs @@ -36,7 +36,7 @@ use std::{ use eyeball::Subscriber; use eyeball_im::VectorDiff; use matrix_sdk_base::{ - deserialized_responses::{AmbiguityChange, SyncTimelineEvent, TimelineEvent}, + deserialized_responses::{AmbiguityChange, TimelineEvent}, event_cache::store::{EventCacheStoreError, EventCacheStoreLock}, store_locks::LockStoreError, sync::RoomUpdates, @@ -214,7 +214,7 @@ impl EventCache { /// Try to find an event by its ID in all the rooms. // Note: replace this with a select-by-id query when this is implemented in a // store. - pub async fn event(&self, event_id: &EventId) -> Option { + pub async fn event(&self, event_id: &EventId) -> Option { self.inner .all_events .read() @@ -323,7 +323,7 @@ impl EventCache { pub async fn add_initial_events( &self, room_id: &RoomId, - events: Vec, + events: Vec, prev_batch: Option, ) -> Result<()> { // If the event cache's storage has been enabled, do nothing. @@ -352,7 +352,7 @@ impl EventCache { } } -type AllEventsMap = BTreeMap; +type AllEventsMap = BTreeMap; type RelationsMap = BTreeMap>; /// Cache wrapper containing both copies of received events and lists of event @@ -374,7 +374,7 @@ impl AllEventsCache { /// If the event is related to another one, its id is added to the relations /// map. - fn append_related_event(&mut self, event: &SyncTimelineEvent) { + fn append_related_event(&mut self, event: &TimelineEvent) { // Handle and cache events and relations. let Ok(AnySyncTimelineEvent::MessageLike(ev)) = event.raw().deserialize() else { return; @@ -452,7 +452,7 @@ impl AllEventsCache { &self, event_id: &EventId, filter: Option<&[RelationType]>, - ) -> Vec { + ) -> Vec { let mut results = Vec::new(); self.collect_related_events_rec(event_id, filter, &mut results); results @@ -462,7 +462,7 @@ impl AllEventsCache { &self, event_id: &EventId, filter: Option<&[RelationType]>, - results: &mut Vec, + results: &mut Vec, ) { let Some(related_event_ids) = self.relations.get(event_id) else { return; @@ -689,7 +689,7 @@ pub enum RoomEventCacheUpdate { /// The room has received updates for the timeline as _diffs_. UpdateTimelineEvents { /// Diffs to apply to the timeline. - diffs: Vec>, + diffs: Vec>, /// Where the diffs are coming from. origin: EventsOrigin, diff --git a/crates/matrix-sdk/src/event_cache/pagination.rs b/crates/matrix-sdk/src/event_cache/pagination.rs index d3ef7fab3..29c402292 100644 --- a/crates/matrix-sdk/src/event_cache/pagination.rs +++ b/crates/matrix-sdk/src/event_cache/pagination.rs @@ -17,9 +17,8 @@ use std::{future::Future, ops::ControlFlow, sync::Arc, time::Duration}; use eyeball::Subscriber; -use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; +use matrix_sdk_base::{deserialized_responses::TimelineEvent, timeout::timeout}; use matrix_sdk_common::linked_chunk::ChunkContent; -use tokio::time::timeout; use tracing::{debug, instrument, trace}; use super::{ @@ -181,7 +180,7 @@ impl RoomPagination { // (backward). The `RoomEvents` API expects the first event to be the oldest. .rev() .cloned() - .map(SyncTimelineEvent::from) + .map(TimelineEvent::from) .collect::>(); let first_event_pos = room_events.events().next().map(|(item_pos, _)| item_pos); @@ -295,7 +294,7 @@ impl RoomPagination { // Otherwise, wait for a notification that we received a previous-batch token. // Note the state lock is released while doing so, allowing other tasks to write // into the linked chunk. - let _ = timeout(wait_time, self.inner.pagination_batch_token_notifier.notified()).await; + let _ = timeout(self.inner.pagination_batch_token_notifier.notified(), wait_time).await; let mut state = self.inner.state.write().await; @@ -453,8 +452,9 @@ mod tests { .write() .await .with_events_mut(|events| { - events - .push_events([f.text_msg("this is the start of the timeline").into_sync()]); + events.push_events([f + .text_msg("this is the start of the timeline") + .into_event()]); }) .await .unwrap(); @@ -498,7 +498,7 @@ mod tests { .with_events_mut(|events| { events.push_events([f .text_msg("this is the start of the timeline") - .into_sync()]); + .into_event()]); }) .await .unwrap(); @@ -542,7 +542,7 @@ mod tests { .text_msg("yolo") .sender(user_id!("@b:z.h")) .event_id(event_id!("$ida")) - .into_sync()]); + .into_event()]); }) .await .unwrap(); diff --git a/crates/matrix-sdk/src/event_cache/paginator.rs b/crates/matrix-sdk/src/event_cache/paginator.rs index 4df79363e..599faf19c 100644 --- a/crates/matrix-sdk/src/event_cache/paginator.rs +++ b/crates/matrix-sdk/src/event_cache/paginator.rs @@ -609,7 +609,7 @@ mod tests { .event_factory .text_msg(self.target_event_text.lock().await.clone()) .event_id(event_id) - .into_timeline(); + .into_event(); // Properly simulate `num_events`: take either the closest num_events events // before, or use all of the before events and then consume after events. @@ -707,17 +707,10 @@ mod tests { *room.target_event_text.lock().await = "fetch_from".to_owned(); *room.prev_events.lock().await = (0..10) .rev() - .map(|i| { - TimelineEvent::new( - event_factory.text_msg(format!("before-{i}")).into_raw_timeline(), - ) - }) - .collect(); - *room.next_events.lock().await = (0..10) - .map(|i| { - TimelineEvent::new(event_factory.text_msg(format!("after-{i}")).into_raw_timeline()) - }) + .map(|i| event_factory.text_msg(format!("before-{i}")).into_event()) .collect(); + *room.next_events.lock().await = + (0..10).map(|i| event_factory.text_msg(format!("after-{i}")).into_event()).collect(); // When I call `Paginator::start_from`, it works, let paginator = Arc::new(Paginator::new(room.clone())); @@ -753,12 +746,8 @@ mod tests { let event_factory = &room.event_factory; *room.target_event_text.lock().await = "fetch_from".to_owned(); - *room.prev_events.lock().await = (0..100) - .rev() - .map(|i| { - TimelineEvent::new(event_factory.text_msg(format!("ev{i}")).into_raw_timeline()) - }) - .collect(); + *room.prev_events.lock().await = + (0..100).rev().map(|i| event_factory.text_msg(format!("ev{i}")).into_event()).collect(); // When I call `Paginator::start_from`, it works, let paginator = Arc::new(Paginator::new(room.clone())); @@ -811,7 +800,7 @@ mod tests { assert!(paginator.hit_timeline_end()); // Preparing data for the next back-pagination. - *room.prev_events.lock().await = vec![event_factory.text_msg("previous").into_timeline()]; + *room.prev_events.lock().await = vec![event_factory.text_msg("previous").into_event()]; *room.prev_batch_token.lock().await = Some("prev2".to_owned()); // When I backpaginate, I get the events I expect. @@ -824,7 +813,7 @@ mod tests { // And I can backpaginate again, because there's a prev batch token // still. - *room.prev_events.lock().await = vec![event_factory.text_msg("oldest").into_timeline()]; + *room.prev_events.lock().await = vec![event_factory.text_msg("oldest").into_event()]; *room.prev_batch_token.lock().await = None; let prev = paginator @@ -875,9 +864,7 @@ mod tests { // Preparing data for the next back-pagination. *room.prev_events.lock().await = (0..100) .rev() - .map(|i| { - TimelineEvent::new(event_factory.text_msg(format!("prev{i}")).into_raw_timeline()) - }) + .map(|i| event_factory.text_msg(format!("prev{i}")).into_event()) .collect(); *room.prev_batch_token.lock().await = None; @@ -927,7 +914,7 @@ mod tests { assert!(!paginator.hit_timeline_end()); // Preparing data for the next forward-pagination. - *room.next_events.lock().await = vec![event_factory.text_msg("next").into_timeline()]; + *room.next_events.lock().await = vec![event_factory.text_msg("next").into_event()]; *room.next_batch_token.lock().await = Some("next2".to_owned()); // When I forward-paginate, I get the events I expect. @@ -940,7 +927,7 @@ mod tests { // And I can forward-paginate again, because there's a prev batch token // still. - *room.next_events.lock().await = vec![event_factory.text_msg("latest").into_timeline()]; + *room.next_events.lock().await = vec![event_factory.text_msg("latest").into_event()]; *room.next_batch_token.lock().await = None; let next = paginator diff --git a/crates/matrix-sdk/src/event_cache/room/events.rs b/crates/matrix-sdk/src/event_cache/room/events.rs index 142594ecb..1b3a6cec5 100644 --- a/crates/matrix-sdk/src/event_cache/room/events.rs +++ b/crates/matrix-sdk/src/event_cache/room/events.rs @@ -557,7 +557,7 @@ mod tests { .text_msg("") .sender(user_id!("@mnt_io:matrix.org")) .event_id(&event_id) - .into_sync(); + .into_event(); (event_id, event) } diff --git a/crates/matrix-sdk/src/event_cache/room/mod.rs b/crates/matrix-sdk/src/event_cache/room/mod.rs index 93a5f4280..3471f4a57 100644 --- a/crates/matrix-sdk/src/event_cache/room/mod.rs +++ b/crates/matrix-sdk/src/event_cache/room/mod.rs @@ -18,7 +18,7 @@ use std::{collections::BTreeMap, fmt, sync::Arc}; use events::Gap; use matrix_sdk_base::{ - deserialized_responses::{AmbiguityChange, SyncTimelineEvent}, + deserialized_responses::{AmbiguityChange, TimelineEvent}, linked_chunk::ChunkContent, sync::{JoinedRoomUpdate, LeftRoomUpdate, Timeline}, }; @@ -77,9 +77,7 @@ impl RoomEventCache { /// Subscribe to this room updates, after getting the initial list of /// events. - pub async fn subscribe( - &self, - ) -> Result<(Vec, Receiver)> { + pub async fn subscribe(&self) -> Result<(Vec, Receiver)> { let state = self.inner.state.read().await; let events = state.events().events().map(|(_position, item)| item.clone()).collect(); @@ -93,7 +91,7 @@ impl RoomEventCache { } /// Try to find an event by id in this room. - pub async fn event(&self, event_id: &EventId) -> Option { + pub async fn event(&self, event_id: &EventId) -> Option { if let Some((room_id, event)) = self.inner.all_events.read().await.events.get(event_id).cloned() { @@ -119,7 +117,7 @@ impl RoomEventCache { &self, event_id: &EventId, filter: Option>, - ) -> Option<(SyncTimelineEvent, Vec)> { + ) -> Option<(TimelineEvent, Vec)> { let cache = self.inner.all_events.read().await; if let Some((_, event)) = cache.events.get(event_id) { let related_events = cache.collect_related_events(event_id, filter.as_deref()); @@ -155,7 +153,7 @@ impl RoomEventCache { // TODO: This doesn't insert the event into the linked chunk. In the future // there'll be no distinction between the linked chunk and the separate // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. - pub(crate) async fn save_event(&self, event: SyncTimelineEvent) { + pub(crate) async fn save_event(&self, event: TimelineEvent) { if let Some(event_id) = event.event_id() { let mut cache = self.inner.all_events.write().await; @@ -172,7 +170,7 @@ impl RoomEventCache { // TODO: This doesn't insert the event into the linked chunk. In the future // there'll be no distinction between the linked chunk and the separate // cache. There is a discussion in https://github.com/matrix-org/matrix-rust-sdk/issues/3886. - pub(crate) async fn save_events(&self, events: impl IntoIterator) { + pub(crate) async fn save_events(&self, events: impl IntoIterator) { let mut cache = self.inner.all_events.write().await; for event in events { if let Some(event_id) = event.event_id() { @@ -370,7 +368,7 @@ impl RoomEventCacheInner { /// storage, notifying observers. pub(super) async fn replace_all_events_by( &self, - sync_timeline_events: Vec, + sync_timeline_events: Vec, prev_batch: Option, ephemeral_events: Vec>, ambiguity_changes: BTreeMap, @@ -407,7 +405,7 @@ impl RoomEventCacheInner { async fn append_events_locked( &self, state: &mut RoomEventCacheState, - sync_timeline_events: Vec, + sync_timeline_events: Vec, prev_batch: Option, ephemeral_events: Vec>, ambiguity_changes: BTreeMap, @@ -506,7 +504,7 @@ impl RoomEventCacheInner { } /// Create a debug string for a [`ChunkContent`] for an event/gap pair. -fn chunk_debug_string(content: &ChunkContent) -> String { +fn chunk_debug_string(content: &ChunkContent) -> String { match content { ChunkContent::Gap(Gap { prev_token }) => { format!("gap['{prev_token}']") @@ -534,7 +532,7 @@ mod private { use eyeball_im::VectorDiff; use matrix_sdk_base::{ - deserialized_responses::{SyncTimelineEvent, TimelineEventKind}, + deserialized_responses::{TimelineEvent, TimelineEventKind}, event_cache::{ store::{ EventCacheStoreError, EventCacheStoreLock, EventCacheStoreLockGuard, @@ -650,7 +648,7 @@ mod private { let _ = closure(); } - fn strip_relations_from_event(ev: &mut SyncTimelineEvent) { + fn strip_relations_from_event(ev: &mut TimelineEvent) { match &mut ev.kind { TimelineEventKind::Decrypted(decrypted) => { // Remove all information about encryption info for @@ -669,7 +667,7 @@ mod private { } /// Strips the bundled relations from a collection of events. - fn strip_relations_from_events(items: &mut [SyncTimelineEvent]) { + fn strip_relations_from_events(items: &mut [TimelineEvent]) { for ev in items.iter_mut() { Self::strip_relations_from_event(ev); } @@ -755,7 +753,7 @@ mod private { pub async fn with_events_mut O>( &mut self, func: F, - ) -> Result<(O, Vec>), EventCacheError> { + ) -> Result<(O, Vec>), EventCacheError> { let output = func(&mut self.events); self.propagate_changes().await?; let updates_as_vector_diffs = self.events.updates_as_vector_diffs(); @@ -807,8 +805,8 @@ mod private { content: ChunkContent::Items(vec![ f.text_msg("hey") .event_id(event_id!("$123456789101112131415617181920")) - .into_sync(), - f.text_msg("you").event_id(event_id!("$2")).into_sync(), + .into_event(), + f.text_msg("you").event_id(event_id!("$2")).into_event(), ]), identifier: CId::new(1), previous: Some(CId::new(0)), @@ -847,7 +845,7 @@ mod tests { store::StoreConfig, sync::{JoinedRoomUpdate, Timeline}, }; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_test::{async_test, event_factory::EventFactory, ALICE, BOB}; use ruma::{ event_id, @@ -1127,7 +1125,7 @@ mod tests { let timeline = Timeline { limited: true, prev_batch: Some("raclette".to_owned()), - events: vec![f.text_msg("hey yo").sender(*ALICE).into_sync()], + events: vec![f.text_msg("hey yo").sender(*ALICE).into_event()], }; room_event_cache @@ -1199,7 +1197,7 @@ mod tests { let mut relations = BundledMessageLikeRelations::new(); relations.replace = Some(Box::new(f.text_msg("Hello, Kind Sir").sender(*ALICE).into_raw_sync())); - let ev = f.text_msg("hey yo").sender(*ALICE).bundled_relations(relations).into_sync(); + let ev = f.text_msg("hey yo").sender(*ALICE).bundled_relations(relations).into_event(); let timeline = Timeline { limited: false, prev_batch: None, events: vec![ev] }; @@ -1263,8 +1261,8 @@ mod tests { let event_id1 = event_id!("$1"); let event_id2 = event_id!("$2"); - let ev1 = f.text_msg("hello world").sender(*ALICE).event_id(event_id1).into_sync(); - let ev2 = f.text_msg("how's it going").sender(*BOB).event_id(event_id2).into_sync(); + let ev1 = f.text_msg("hello world").sender(*ALICE).event_id(event_id1).into_event(); + let ev2 = f.text_msg("how's it going").sender(*BOB).event_id(event_id2).into_event(); // Prefill the store with some data. event_cache_store @@ -1374,8 +1372,8 @@ mod tests { let event_id1 = event_id!("$1"); let event_id2 = event_id!("$2"); - let ev1 = f.text_msg("hello world").sender(*ALICE).event_id(event_id1).into_sync(); - let ev2 = f.text_msg("how's it going").sender(*BOB).event_id(event_id2).into_sync(); + let ev1 = f.text_msg("hello world").sender(*ALICE).event_id(event_id1).into_event(); + let ev2 = f.text_msg("how's it going").sender(*BOB).event_id(event_id2).into_event(); // Prefill the store with some data. event_cache_store @@ -1549,7 +1547,7 @@ mod tests { timeline: Timeline { limited: true, prev_batch: Some("raclette".to_owned()), - events: vec![f.text_msg("hey yo").into_sync()], + events: vec![f.text_msg("hey yo").into_event()], }, ..Default::default() }, @@ -1585,7 +1583,7 @@ mod tests { timeline: Timeline { limited: false, prev_batch: Some("fondue".to_owned()), - events: vec![f.text_msg("sup").into_sync()], + events: vec![f.text_msg("sup").into_event()], }, ..Default::default() }, @@ -1617,8 +1615,8 @@ mod tests { async fn assert_relations( room_id: &RoomId, - original_event: SyncTimelineEvent, - related_event: SyncTimelineEvent, + original_event: TimelineEvent, + related_event: TimelineEvent, event_factory: EventFactory, ) { let client = logged_in_client(None).await; diff --git a/crates/matrix-sdk/src/event_handler/mod.rs b/crates/matrix-sdk/src/event_handler/mod.rs index 5fff2db8b..a8b8d2946 100644 --- a/crates/matrix-sdk/src/event_handler/mod.rs +++ b/crates/matrix-sdk/src/event_handler/mod.rs @@ -50,7 +50,7 @@ use eyeball::{SharedObservable, Subscriber}; use futures_core::Stream; use futures_util::stream::{FuturesUnordered, StreamExt}; use matrix_sdk_base::{ - deserialized_responses::{EncryptionInfo, SyncTimelineEvent}, + deserialized_responses::{EncryptionInfo, TimelineEvent}, SendOutsideWasm, SyncOutsideWasm, }; use pin_project_lite::pin_project; @@ -380,7 +380,7 @@ impl Client { pub(crate) async fn handle_sync_timeline_events( &self, room: Option<&Room>, - timeline_events: &[SyncTimelineEvent], + timeline_events: &[TimelineEvent], ) -> serde_json::Result<()> { #[derive(Deserialize)] struct TimelineEventDetails<'a> { @@ -402,7 +402,7 @@ impl Client { let raw_event = item.raw().json(); let encryption_info = item.encryption_info(); - let push_actions = &item.push_actions; + let push_actions = item.push_actions.as_deref().unwrap_or(&[]); // Event handlers for possibly-redacted timeline events self.call_event_handlers( diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index bfc7a9bc2..4f4d4b217 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -45,11 +45,8 @@ mod error; pub mod event_cache; pub mod event_handler; mod http_client; -pub mod matrix_auth; pub mod media; pub mod notification_settings; -#[cfg(feature = "experimental-oidc")] -pub mod oidc; pub mod pusher; pub mod room; pub mod room_directory_search; diff --git a/crates/matrix-sdk/src/room/edit.rs b/crates/matrix-sdk/src/room/edit.rs index 850e70d38..0b3844afe 100644 --- a/crates/matrix-sdk/src/room/edit.rs +++ b/crates/matrix-sdk/src/room/edit.rs @@ -16,7 +16,7 @@ use std::future::Future; -use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, SendOutsideWasm}; +use matrix_sdk_base::{deserialized_responses::TimelineEvent, SendOutsideWasm}; use ruma::{ events::{ poll::unstable_start::{ @@ -129,11 +129,11 @@ trait EventSource { fn get_event( &self, event_id: &EventId, - ) -> impl Future> + SendOutsideWasm; + ) -> impl Future> + SendOutsideWasm; } impl EventSource for &Room { - async fn get_event(&self, event_id: &EventId) -> Result { + async fn get_event(&self, event_id: &EventId) -> Result { match self.event_cache().await { Ok((event_cache, _drop_handles)) => { if let Some(event) = event_cache.event(event_id).await { @@ -359,7 +359,7 @@ mod tests { use std::collections::BTreeMap; use assert_matches2::{assert_let, assert_matches}; - use matrix_sdk_base::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_base::deserialized_responses::TimelineEvent; use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{ event_id, @@ -378,11 +378,11 @@ mod tests { #[derive(Default)] struct TestEventCache { - events: BTreeMap, + events: BTreeMap, } impl EventSource for TestEventCache { - async fn get_event(&self, event_id: &EventId) -> Result { + async fn get_event(&self, event_id: &EventId) -> Result { Ok(self.events.get(event_id).unwrap().clone()) } } @@ -397,7 +397,7 @@ mod tests { cache.events.insert( event_id.to_owned(), // TODO: use the EventFactory for state events too. - SyncTimelineEvent::new( + TimelineEvent::new( Raw::::from_json_string( json!({ "content": { @@ -589,7 +589,7 @@ mod tests { .caption(Some("caption".to_owned()), None) .event_id(event_id) .sender(own_user_id) - .into_sync(); + .into_event(); { // Sanity checks. @@ -648,7 +648,7 @@ mod tests { .image(filename.to_owned(), owned_mxc_uri!("mxc://sdk.rs/rickroll")) .event_id(event_id) .sender(own_user_id) - .into_sync(); + .into_event(); { // Sanity checks. diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index bc9a2a6db..c351d6e05 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -38,7 +38,7 @@ use matrix_sdk_base::crypto::{DecryptionSettings, RoomEventDecryptionResult}; use matrix_sdk_base::crypto::{IdentityStatusChange, RoomIdentityProvider, UserIdentity}; use matrix_sdk_base::{ deserialized_responses::{ - RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, TimelineEvent, + RawAnySyncOrStrippedState, RawSyncOrStrippedState, SyncOrStrippedState, }, event_cache::store::media::IgnoreMediaRetentionPolicy, media::MediaThumbnailSettings, @@ -49,7 +49,7 @@ use matrix_sdk_base::{ #[cfg(all(feature = "e2e-encryption", not(target_arch = "wasm32")))] use matrix_sdk_common::BoxFuture; use matrix_sdk_common::{ - deserialized_responses::SyncTimelineEvent, + deserialized_responses::TimelineEvent, executor::{spawn, JoinHandle}, timeout::timeout, }; @@ -327,7 +327,11 @@ impl Room { start: http_response.start, end: http_response.end, #[cfg(not(feature = "e2e-encryption"))] - chunk: http_response.chunk.into_iter().map(TimelineEvent::new).collect(), + chunk: http_response + .chunk + .into_iter() + .map(|raw| TimelineEvent::new(raw.cast())) + .collect(), #[cfg(feature = "e2e-encryption")] chunk: Vec::with_capacity(http_response.chunk.len()), state: http_response.state, @@ -342,10 +346,10 @@ impl Room { if let Ok(event) = self.decrypt_event(event.cast_ref()).await { event } else { - TimelineEvent::new(event) + TimelineEvent::new(event.cast()) } } else { - TimelineEvent::new(event) + TimelineEvent::new(event.cast()) }; response.chunk.push(decrypted_event); } @@ -459,7 +463,7 @@ impl Room { } } - let mut event = TimelineEvent::new(event); + let mut event = TimelineEvent::new(event.cast()); event.push_actions = self.event_push_actions(event.raw()).await?; Ok(event) @@ -482,7 +486,7 @@ impl Room { // Save the event into the event cache, if it's set up. if let Ok((cache, _handles)) = self.event_cache().await { - cache.save_event(event.clone().into()).await; + cache.save_event(event.clone()).await; } Ok(event) @@ -526,17 +530,17 @@ impl Room { // Save the loaded events into the event cache, if it's set up. if let Ok((cache, _handles)) = self.event_cache().await { - let mut events_to_save: Vec = Vec::new(); + let mut events_to_save: Vec = Vec::new(); if let Some(event) = &target_event { - events_to_save.push(event.clone().into()); + events_to_save.push(event.clone()); } for event in &events_before { - events_to_save.push(event.clone().into()); + events_to_save.push(event.clone()); } for event in &events_after { - events_to_save.push(event.clone().into()); + events_to_save.push(event.clone()); } cache.save_events(events_to_save).await; @@ -3709,8 +3713,8 @@ mod tests { use super::ReportedContentScore; use crate::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{logged_in_client, mocks::MatrixMockServer}, Client, }; diff --git a/crates/matrix-sdk/src/sliding_sync/client.rs b/crates/matrix-sdk/src/sliding_sync/client.rs index 2cad13477..6f9935819 100644 --- a/crates/matrix-sdk/src/sliding_sync/client.rs +++ b/crates/matrix-sdk/src/sliding_sync/client.rs @@ -257,7 +257,7 @@ impl PreviousEventsProvider for SlidingSyncPreviousEventsProvider<'_> { fn for_room( &self, room_id: &ruma::RoomId, - ) -> Vector { + ) -> Vector { self.0.get(room_id).map(|room| room.timeline_queue()).unwrap_or_default() } } @@ -380,8 +380,8 @@ mod tests { use super::{discover_homeserver, get_supported_versions, Version, VersionBuilder}; use crate::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, error::Result, - matrix_auth::{MatrixSession, MatrixSessionTokens}, sliding_sync::{http, VersionBuilderError}, test_utils::logged_in_client_with_server, Client, SlidingSyncList, SlidingSyncMode, diff --git a/crates/matrix-sdk/src/sliding_sync/mod.rs b/crates/matrix-sdk/src/sliding_sync/mod.rs index cfdf45105..2256e5c43 100644 --- a/crates/matrix-sdk/src/sliding_sync/mod.rs +++ b/crates/matrix-sdk/src/sliding_sync/mod.rs @@ -36,7 +36,7 @@ use async_stream::stream; pub use client::{Version, VersionBuilder}; use futures_core::stream::Stream; pub use matrix_sdk_base::sliding_sync::http; -use matrix_sdk_common::{deserialized_responses::SyncTimelineEvent, executor::spawn, timer}; +use matrix_sdk_common::{deserialized_responses::TimelineEvent, executor::spawn, timer}; use ruma::{ api::{client::error::ErrorKind, OutgoingRequest}, assign, OwnedEventId, OwnedRoomId, RoomId, @@ -345,7 +345,7 @@ impl SlidingSync { if let Some(joined_room) = sync_response.rooms.join.remove(&room_id) { joined_room.timeline.events } else { - room_data.timeline.drain(..).map(SyncTimelineEvent::new).collect() + room_data.timeline.drain(..).map(TimelineEvent::new).collect() }; match rooms_map.get_mut(&room_id) { @@ -1103,7 +1103,7 @@ mod tests { use assert_matches::assert_matches; use event_listener::Listener; use futures_util::{future::join_all, pin_mut, StreamExt}; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_test::async_test; use ruma::{ api::client::error::ErrorKind, assign, owned_room_id, room_id, serde::Raw, uint, @@ -2268,8 +2268,8 @@ mod tests { #[async_test] async fn test_limited_flag_computation() { - let make_event = |event_id: &str| -> SyncTimelineEvent { - SyncTimelineEvent::new( + let make_event = |event_id: &str| -> TimelineEvent { + TimelineEvent::new( Raw::from_json_string( json!({ "event_id": event_id, diff --git a/crates/matrix-sdk/src/sliding_sync/room.rs b/crates/matrix-sdk/src/sliding_sync/room.rs index b78cdc147..49ce04a0f 100644 --- a/crates/matrix-sdk/src/sliding_sync/room.rs +++ b/crates/matrix-sdk/src/sliding_sync/room.rs @@ -4,7 +4,7 @@ use std::{ }; use eyeball_im::Vector; -use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, sliding_sync::http}; +use matrix_sdk_base::{deserialized_responses::TimelineEvent, sliding_sync::http}; use ruma::{OwnedRoomId, RoomId}; use serde::{Deserialize, Serialize}; @@ -40,7 +40,7 @@ impl SlidingSyncRoom { pub fn new( room_id: OwnedRoomId, prev_batch: Option, - timeline: Vec, + timeline: Vec, ) -> Self { Self { inner: Arc::new(SlidingSyncRoomInner { @@ -66,14 +66,14 @@ impl SlidingSyncRoom { /// /// Note: This API only exists temporarily, it *will* be removed in the /// future. - pub fn timeline_queue(&self) -> Vector { + pub fn timeline_queue(&self) -> Vector { self.inner.timeline_queue.read().unwrap().clone() } pub(super) fn update( &mut self, room_data: http::response::Room, - timeline_updates: Vec, + timeline_updates: Vec, ) { let http::response::Room { prev_batch, limited, .. } = room_data; @@ -165,7 +165,7 @@ struct SlidingSyncRoomInner { /// /// When persisting the room, this queue is truncated to keep only the last /// N events. - timeline_queue: RwLock>, + timeline_queue: RwLock>, } /// A “frozen” [`SlidingSyncRoom`], i.e. that can be written into, or read from @@ -176,7 +176,7 @@ pub(super) struct FrozenSlidingSyncRoom { #[serde(skip_serializing_if = "Option::is_none")] pub(super) prev_batch: Option, #[serde(rename = "timeline")] - pub(super) timeline_queue: Vector, + pub(super) timeline_queue: Vector, } /// Number of timeline events to keep when [`SlidingSyncRoom`] is saved in the @@ -214,8 +214,7 @@ impl From<&SlidingSyncRoom> for FrozenSlidingSyncRoom { #[cfg(test)] mod tests { use imbl::vector; - use matrix_sdk_base::deserialized_responses::TimelineEvent; - use matrix_sdk_common::deserialized_responses::SyncTimelineEvent; + use matrix_sdk_common::deserialized_responses::TimelineEvent; use matrix_sdk_test::async_test; use ruma::{events::room::message::RoomMessageEventContent, room_id, serde::Raw, RoomId}; use serde_json::json; @@ -238,7 +237,7 @@ mod tests { fn new_room_with_timeline( room_id: &RoomId, inner: http::response::Room, - timeline: Vec, + timeline: Vec, ) -> SlidingSyncRoom { SlidingSyncRoom::new(room_id.to_owned(), inner.prev_batch, timeline) } @@ -326,7 +325,7 @@ mod tests { })) .unwrap() .cast() - ).into() + ) }; } @@ -617,8 +616,7 @@ mod tests { })) .unwrap() .cast(), - ) - .into()], + )], }; let serialized = serde_json::to_value(&frozen_room).unwrap(); @@ -670,7 +668,6 @@ mod tests { .unwrap() .cast(), ) - .into() }) .collect::>(); @@ -707,7 +704,6 @@ mod tests { .unwrap() .cast(), ) - .into() }) .collect::>(); diff --git a/crates/matrix-sdk/src/sync.rs b/crates/matrix-sdk/src/sync.rs index 5a824ebf8..61f54ccfe 100644 --- a/crates/matrix-sdk/src/sync.rs +++ b/crates/matrix-sdk/src/sync.rs @@ -23,6 +23,7 @@ use std::{ pub use matrix_sdk_base::sync::*; use matrix_sdk_base::{ debug::{DebugInvitedRoom, DebugKnockedRoom, DebugListOfRawEventsNoId}, + sleep::sleep, sync::SyncResponse as BaseSyncResponse, }; use ruma::{ @@ -299,11 +300,7 @@ impl Client { } async fn sleep() { - #[cfg(target_arch = "wasm32")] - gloo_timers::future::TimeoutFuture::new(1_000).await; - - #[cfg(not(target_arch = "wasm32"))] - tokio::time::sleep(Duration::from_secs(1)).await; + sleep(Duration::from_secs(1)).await; } pub(crate) async fn sync_loop_helper( diff --git a/crates/matrix-sdk/src/test_utils/client.rs b/crates/matrix-sdk/src/test_utils/client.rs index bfbbf539e..b22494077 100644 --- a/crates/matrix-sdk/src/test_utils/client.rs +++ b/crates/matrix-sdk/src/test_utils/client.rs @@ -18,8 +18,8 @@ use matrix_sdk_base::{store::StoreConfig, SessionMeta}; use ruma::{api::MatrixVersion, device_id, user_id}; use crate::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, Client, ClientBuilder, }; diff --git a/crates/matrix-sdk/src/test_utils/mod.rs b/crates/matrix-sdk/src/test_utils/mod.rs index 80865ad2a..0594bb134 100644 --- a/crates/matrix-sdk/src/test_utils/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mod.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use assert_matches2::assert_let; -use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, SessionMeta}; +use matrix_sdk_base::{deserialized_responses::TimelineEvent, SessionMeta}; use ruma::{ api::MatrixVersion, device_id, @@ -17,15 +17,15 @@ pub mod client; pub mod mocks; use crate::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, Client, ClientBuilder, }; /// Checks that an event is a message-like text event with the given text. #[track_caller] -pub fn assert_event_matches_msg>(event: &E, expected: &str) { - let event: SyncTimelineEvent = event.clone().into(); +pub fn assert_event_matches_msg>(event: &E, expected: &str) { + let event: TimelineEvent = event.clone().into(); let event = event.raw().deserialize().unwrap(); assert_let!( AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) = event diff --git a/crates/matrix-sdk/tests/integration/client.rs b/crates/matrix-sdk/tests/integration/client.rs index 2269d0018..aafaaa3fa 100644 --- a/crates/matrix-sdk/tests/integration/client.rs +++ b/crates/matrix-sdk/tests/integration/client.rs @@ -4,8 +4,8 @@ use assert_matches2::{assert_let, assert_matches}; use eyeball_im::VectorDiff; use futures_util::FutureExt; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::{RequestConfig, StoreConfig, SyncSettings}, - matrix_auth::{MatrixSession, MatrixSessionTokens}, sync::RoomUpdate, test_utils::no_retry_test_client_with_server, Client, MemoryStore, SessionMeta, StateChanges, StateStore, diff --git a/crates/matrix-sdk/tests/integration/encryption/backups.rs b/crates/matrix-sdk/tests/integration/encryption/backups.rs index 5f41f1e39..fe56cf0f0 100644 --- a/crates/matrix-sdk/tests/integration/encryption/backups.rs +++ b/crates/matrix-sdk/tests/integration/encryption/backups.rs @@ -18,6 +18,7 @@ use anyhow::Result; use assert_matches::assert_matches; use futures_util::{pin_mut, FutureExt, StreamExt}; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, crypto::{ olm::{InboundGroupSession, SenderData, SessionCreationError}, @@ -29,7 +30,6 @@ use matrix_sdk::{ secret_storage::SecretStore, BackupDownloadStrategy, EncryptionSettings, }, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{no_retry_test_client_with_server, test_client_builder_with_server}, Client, }; diff --git a/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs b/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs index 79122252c..46274a744 100644 --- a/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs +++ b/crates/matrix-sdk/tests/integration/encryption/cross_signing.rs @@ -14,8 +14,8 @@ use assert_matches2::assert_let; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, encryption::CrossSigningResetAuthType, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::no_retry_test_client_with_server, SessionMeta, }; @@ -129,8 +129,8 @@ async fn test_reset_oidc() { registration::{ClientMetadata, VerifiedClientMetadata}, }; use matrix_sdk::{ + authentication::oidc::{OidcSession, OidcSessionTokens, UserSession}, encryption::CrossSigningResetAuthType, - oidc::{OidcSession, OidcSessionTokens, UserSession}, }; use similar_asserts::assert_eq; use url::Url; diff --git a/crates/matrix-sdk/tests/integration/encryption/recovery.rs b/crates/matrix-sdk/tests/integration/encryption/recovery.rs index eada062a1..6f49a4232 100644 --- a/crates/matrix-sdk/tests/integration/encryption/recovery.rs +++ b/crates/matrix-sdk/tests/integration/encryption/recovery.rs @@ -17,13 +17,13 @@ use std::sync::{Arc, Mutex}; use assert_matches2::assert_let; use futures_util::StreamExt; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, encryption::{ backups::BackupState, recovery::{EnableProgress, RecoveryState}, BackupDownloadStrategy, CrossSigningResetAuthType, }, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{no_retry_test_client_with_server, test_client_builder_with_server}, Client, }; diff --git a/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs b/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs index 147ed871b..fb62d506f 100644 --- a/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs +++ b/crates/matrix-sdk/tests/integration/encryption/secret_storage.rs @@ -2,8 +2,8 @@ use std::sync::{Arc, Mutex}; use assert_matches::assert_matches; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, encryption::secret_storage::SecretStorageError, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::no_retry_test_client_with_server, }; use matrix_sdk_base::SessionMeta; diff --git a/crates/matrix-sdk/tests/integration/encryption/verification.rs b/crates/matrix-sdk/tests/integration/encryption/verification.rs index 69bc38514..ece7be7c0 100644 --- a/crates/matrix-sdk/tests/integration/encryption/verification.rs +++ b/crates/matrix-sdk/tests/integration/encryption/verification.rs @@ -7,9 +7,9 @@ use assert_matches2::assert_matches; use futures_util::FutureExt; use imbl::HashSet; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, encryption::VerificationState, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::logged_in_client_with_server, Client, }; diff --git a/crates/matrix-sdk/tests/integration/event_cache.rs b/crates/matrix-sdk/tests/integration/event_cache.rs index a2e2aaf7d..700c61da7 100644 --- a/crates/matrix-sdk/tests/integration/event_cache.rs +++ b/crates/matrix-sdk/tests/integration/event_cache.rs @@ -9,7 +9,7 @@ use assert_matches2::assert_let; use eyeball_im::VectorDiff; use matrix_sdk::{ assert_let_timeout, assert_next_matches_with_timeout, - deserialized_responses::SyncTimelineEvent, + deserialized_responses::TimelineEvent, event_cache::{ paginator::PaginatorState, BackPaginationOutcome, EventCacheError, PaginationToken, RoomEventCacheUpdate, TimelineHasBeenResetWhilePaginating, @@ -204,7 +204,7 @@ async fn test_ignored_unignored() { /// Small helper for backpagination tests, to wait for things to stabilize. async fn wait_for_initial_events( - events: Vec, + events: Vec, room_stream: &mut broadcast::Receiver, ) { if events.is_empty() { diff --git a/crates/matrix-sdk/tests/integration/matrix_auth.rs b/crates/matrix-sdk/tests/integration/matrix_auth.rs index ef732b37f..6bd2e0a16 100644 --- a/crates/matrix-sdk/tests/integration/matrix_auth.rs +++ b/crates/matrix-sdk/tests/integration/matrix_auth.rs @@ -2,8 +2,8 @@ use std::{collections::BTreeMap, sync::Mutex}; use assert_matches::assert_matches; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{logged_in_client_with_server, no_retry_test_client_with_server}, AuthApi, AuthSession, Client, RumaApiError, }; diff --git a/crates/matrix-sdk/tests/integration/media.rs b/crates/matrix-sdk/tests/integration/media.rs index ec7032919..75d78d963 100644 --- a/crates/matrix-sdk/tests/integration/media.rs +++ b/crates/matrix-sdk/tests/integration/media.rs @@ -1,6 +1,6 @@ use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, - matrix_auth::{MatrixSession, MatrixSessionTokens}, media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, test_utils::logged_in_client_with_server, Client, SessionMeta, diff --git a/crates/matrix-sdk/tests/integration/refresh_token.rs b/crates/matrix-sdk/tests/integration/refresh_token.rs index 6067698c6..c2dbcaee6 100644 --- a/crates/matrix-sdk/tests/integration/refresh_token.rs +++ b/crates/matrix-sdk/tests/integration/refresh_token.rs @@ -7,9 +7,9 @@ use assert_matches::assert_matches; use assert_matches2::assert_let; use futures_util::StreamExt; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, config::RequestConfig, executor::spawn, - matrix_auth::{MatrixSession, MatrixSessionTokens}, test_utils::{ logged_in_client_with_server, no_retry_test_client_with_server, test_client_builder_with_server, diff --git a/crates/matrix-sdk/tests/integration/room/joined.rs b/crates/matrix-sdk/tests/integration/room/joined.rs index 6aee69b3d..a55114464 100644 --- a/crates/matrix-sdk/tests/integration/room/joined.rs +++ b/crates/matrix-sdk/tests/integration/room/joined.rs @@ -797,7 +797,7 @@ async fn test_make_reply_event_doesnt_require_event_cache() { let event_id = event_id!("$1"); let f = EventFactory::new(); mock.mock_room_event() - .ok(f.text_msg("hi").event_id(event_id).sender(&user_id).room(room_id).into_timeline()) + .ok(f.text_msg("hi").event_id(event_id).sender(&user_id).room(room_id).into_event()) .expect(1) .named("/event") .mount() diff --git a/crates/matrix-sdk/tests/integration/send_queue.rs b/crates/matrix-sdk/tests/integration/send_queue.rs index a35fcad9d..392448736 100644 --- a/crates/matrix-sdk/tests/integration/send_queue.rs +++ b/crates/matrix-sdk/tests/integration/send_queue.rs @@ -903,7 +903,7 @@ async fn test_edit() { .text_msg("msg1") .sender(client.user_id().unwrap()) .room(room_id) - .into_timeline()) + .into_event()) .expect(1) .named("room_event") .mount() @@ -1010,7 +1010,7 @@ async fn test_edit_with_poll_start() { .poll_start("poll_start", "question", vec!["Answer A"]) .sender(client.user_id().unwrap()) .room(room_id) - .into_timeline()) + .into_event()) .expect(1) .named("get_event") .mount() @@ -2944,7 +2944,7 @@ async fn test_update_caption_while_sending_media_event() { .image("surprise.jpeg.exe".to_owned(), owned_mxc_uri!("mxc://sdk.rs/media")) .sender(client.user_id().unwrap()) .room(room_id) - .into_timeline()) + .into_event()) .expect(1) .named("room_event") .mount() diff --git a/examples/oidc_cli/src/main.rs b/examples/oidc_cli/src/main.rs index 8761bcef3..13d0d82c8 100644 --- a/examples/oidc_cli/src/main.rs +++ b/examples/oidc_cli/src/main.rs @@ -30,9 +30,7 @@ use axum::{ }; use futures_util::StreamExt; use matrix_sdk::{ - config::SyncSettings, - encryption::{recovery::RecoveryState, CrossSigningResetAuthType}, - oidc::{ + authentication::oidc::{ requests::account_management::AccountManagementActionFull, types::{ client_credentials::ClientCredentials, @@ -44,6 +42,8 @@ use matrix_sdk::{ }, AuthorizationCode, AuthorizationResponse, OidcAuthorizationData, OidcSession, UserSession, }, + config::SyncSettings, + encryption::{recovery::RecoveryState, CrossSigningResetAuthType}, room::Room, ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, Client, ClientBuildError, Result, RoomState, diff --git a/examples/persist_session/src/main.rs b/examples/persist_session/src/main.rs index b3b9a9e1c..248612ccc 100644 --- a/examples/persist_session/src/main.rs +++ b/examples/persist_session/src/main.rs @@ -4,8 +4,8 @@ use std::{ }; use matrix_sdk::{ + authentication::matrix::MatrixSession, config::SyncSettings, - matrix_auth::MatrixSession, ruma::{ api::client::filter::FilterDefinition, events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, diff --git a/examples/qr-login/src/main.rs b/examples/qr-login/src/main.rs index 665c511b1..67699bd9c 100644 --- a/examples/qr-login/src/main.rs +++ b/examples/qr-login/src/main.rs @@ -4,12 +4,14 @@ use anyhow::{bail, Context, Result}; use clap::Parser; use futures_util::StreamExt; use matrix_sdk::{ - authentication::qrcode::{LoginProgress, QrCodeData, QrCodeModeData}, - oidc::types::{ - iana::oauth::OAuthClientAuthenticationMethod, - oidc::ApplicationType, - registration::{ClientMetadata, Localized, VerifiedClientMetadata}, - requests::GrantType, + authentication::{ + oidc::types::{ + iana::oauth::OAuthClientAuthenticationMethod, + oidc::ApplicationType, + registration::{ClientMetadata, Localized, VerifiedClientMetadata}, + requests::GrantType, + }, + qrcode::{LoginProgress, QrCodeData, QrCodeModeData}, }, Client, }; diff --git a/examples/secret_storage/src/main.rs b/examples/secret_storage/src/main.rs index 1175c4612..92196fceb 100644 --- a/examples/secret_storage/src/main.rs +++ b/examples/secret_storage/src/main.rs @@ -1,8 +1,8 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use matrix_sdk::{ + authentication::matrix::{MatrixSession, MatrixSessionTokens}, encryption::secret_storage::SecretStore, - matrix_auth::{MatrixSession, MatrixSessionTokens}, ruma::{events::secret::request::SecretName, OwnedDeviceId, OwnedUserId}, AuthSession, Client, SessionMeta, }; diff --git a/examples/timeline/src/main.rs b/examples/timeline/src/main.rs index b36a005a7..5b3d2bc40 100644 --- a/examples/timeline/src/main.rs +++ b/examples/timeline/src/main.rs @@ -75,8 +75,8 @@ async fn main() -> Result<()> { println!("Initial timeline items: {timeline_items:#?}"); tokio::spawn(async move { - while let Some(diff) = timeline_stream.next().await { - println!("Received a timeline diff: {diff:#?}"); + while let Some(diffs) = timeline_stream.next().await { + println!("Received timeline diffs: {diffs:#?}"); } }); diff --git a/labs/multiverse/src/main.rs b/labs/multiverse/src/main.rs index 9da33ecf5..eb18f2f4c 100644 --- a/labs/multiverse/src/main.rs +++ b/labs/multiverse/src/main.rs @@ -17,14 +17,15 @@ use crossterm::{ use futures_util::{pin_mut, StreamExt as _}; use imbl::Vector; use matrix_sdk::{ + authentication::matrix::MatrixSession, config::StoreConfig, encryption::{BackupDownloadStrategy, EncryptionSettings}, - matrix_auth::MatrixSession, ruma::{ api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::{MessageType, RoomMessageEventContent}, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId, }, + sleep::sleep, AuthSession, Client, ServerName, SqliteCryptoStore, SqliteEventCacheStore, SqliteStateStore, }; use matrix_sdk_ui::{ @@ -274,9 +275,12 @@ impl App { let timeline_task = spawn(async move { pin_mut!(stream); let items = i; - while let Some(diff) = stream.next().await { + while let Some(diffs) = stream.next().await { let mut items = items.lock().unwrap(); - diff.apply(&mut items); + + for diff in diffs { + diff.apply(&mut items); + } } }); @@ -329,7 +333,7 @@ impl App { let message = self.last_status_message.clone(); self.clear_status_message = Some(spawn(async move { // Clear the status message in 4 seconds. - tokio::time::sleep(Duration::from_secs(4)).await; + sleep(Duration::from_secs(4)).await; *message.lock().unwrap() = None; })); @@ -414,7 +418,7 @@ impl App { // Start a new one, request batches of 20 events, stop after 10 timeline items // have been added. *pagination = Some(spawn(async move { - if let Err(err) = sdk_timeline.live_paginate_backwards(20).await { + if let Err(err) = sdk_timeline.paginate_backwards(20).await { // TODO: would be nice to be able to set the status // message remotely? //self.set_status_message(format!( diff --git a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs index 7a441e9ec..fa7b3296a 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/sliding_sync/room.rs @@ -908,31 +908,33 @@ async fn test_delayed_invite_response_and_sent_message_decryption() { bob_timeline.paginate_backwards(3).await.unwrap(); // Look for the sent message, which should not be an UTD event. - while let Ok(Some(diff)) = timeout(Duration::from_secs(3), timeline_stream.next()).await { - trace!(?diff, "Received diff from Bob's room"); + while let Ok(Some(diffs)) = timeout(Duration::from_secs(3), timeline_stream.next()).await { + trace!(?diffs, "Received diffs from Bob's room"); - match diff { - VectorDiff::PushFront { value: event } - | VectorDiff::PushBack { value: event } - | VectorDiff::Insert { value: event, .. } - | VectorDiff::Set { value: event, .. } => { - let Some(event) = event.as_event() else { - continue; - }; + for diff in diffs { + match diff { + VectorDiff::PushFront { value: event } + | VectorDiff::PushBack { value: event } + | VectorDiff::Insert { value: event, .. } + | VectorDiff::Set { value: event, .. } => { + let Some(event) = event.as_event() else { + continue; + }; - let content = event.content(); + let content = event.content(); - if content.as_unable_to_decrypt().is_some() { - info!("Observed UTD for {}", event.event_id().unwrap()); + if content.as_unable_to_decrypt().is_some() { + info!("Observed UTD for {}", event.event_id().unwrap()); + } + + if let Some(message) = content.as_message() { + assert_eq!(message.body(), "hello world"); + return; + } } - if let Some(message) = content.as_message() { - assert_eq!(message.body(), "hello world"); - return; - } + _ => {} } - - _ => {} } } diff --git a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs index 40dbe1b8e..a822a4142 100644 --- a/testing/matrix-sdk-integration-testing/src/tests/timeline.rs +++ b/testing/matrix-sdk-integration-testing/src/tests/timeline.rs @@ -55,12 +55,9 @@ use crate::helpers::TestClientBuilder; /// /// A macro to help lowering compile times and getting better error locations. macro_rules! assert_event_is_updated { - ($stream:expr, $event_id:expr, $index:expr) => {{ - assert_let!( - Ok(Some(VectorDiff::Set { index: i, value: event })) = - timeout(Duration::from_secs(1), $stream.next()).await - ); - assert_eq!(i, $index, "unexpected position for event update, value = {event:?}"); + ($diff:expr, $event_id:expr, $index:expr) => {{ + assert_let!(VectorDiff::Set { index: i, value: event } = &$diff); + assert_eq!(*i, $index, "unexpected position for event update, value = {event:?}"); let event = event.as_event().unwrap(); assert_eq!(event.event_id().unwrap(), $event_id); @@ -114,9 +111,13 @@ async fn test_toggling_reaction() -> Result<()> { warn!(?items, "Waiting for updates…"); - while let Some(diff) = stream.next().await { - warn!(?diff, "received a diff"); - diff.apply(&mut items); + while let Some(diffs) = stream.next().await { + warn!(?diffs, "received diffs"); + + for diff in diffs { + diff.apply(&mut items); + } + if let Some(event_id) = find_event_id(&items) { return Ok(event_id); } @@ -149,8 +150,10 @@ async fn test_toggling_reaction() -> Result<()> { // Skip all stream updates that have happened so far. debug!("Skipping all other stream updates…"); - while let Some(Some(diff)) = stream.next().now_or_never() { - diff.apply(&mut items); + while let Some(Some(diffs)) = stream.next().now_or_never() { + for diff in diffs { + diff.apply(&mut items); + } } let (message_position, item_id) = items @@ -171,9 +174,14 @@ async fn test_toggling_reaction() -> Result<()> { // Add the reaction. timeline.toggle_reaction(&item_id, &reaction_key).await.expect("toggling reaction"); + sleep(Duration::from_secs(1)).await; + + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 2); + // Local echo is added. { - let event = assert_event_is_updated!(stream, event_id, message_position); + let event = assert_event_is_updated!(timeline_updates[0], event_id, message_position); let reactions = event.reactions().get(&reaction_key).unwrap(); let reaction = reactions.get(&user_id).unwrap(); assert_matches!(reaction.status, ReactionStatus::LocalToRemote(..)); @@ -181,7 +189,7 @@ async fn test_toggling_reaction() -> Result<()> { // Remote echo is added. { - let event = assert_event_is_updated!(stream, event_id, message_position); + let event = assert_event_is_updated!(timeline_updates[1], event_id, message_position); let reactions = event.reactions().get(&reaction_key).unwrap(); assert_eq!(reactions.keys().count(), 1); @@ -202,11 +210,16 @@ async fn test_toggling_reaction() -> Result<()> { .await .expect("toggling reaction the second time"); + sleep(Duration::from_secs(1)).await; + + assert_let!(Some(timeline_updates) = stream.next().await); + assert_eq!(timeline_updates.len(), 1); + // The reaction is removed. - let event = assert_event_is_updated!(stream, event_id, message_position); + let event = assert_event_is_updated!(timeline_updates[0], event_id, message_position); assert!(event.reactions().is_empty()); - assert!(stream.next().now_or_never().is_none()); + assert_pending!(stream); } Ok(()) diff --git a/testing/matrix-sdk-test/src/event_factory.rs b/testing/matrix-sdk-test/src/event_factory.rs index 48ee5afde..61ea252e0 100644 --- a/testing/matrix-sdk-test/src/event_factory.rs +++ b/testing/matrix-sdk-test/src/event_factory.rs @@ -21,7 +21,7 @@ use std::{ use as_variant::as_variant; use matrix_sdk_common::deserialized_responses::{ - SyncTimelineEvent, TimelineEvent, UnableToDecryptInfo, UnableToDecryptReason, + TimelineEvent, UnableToDecryptInfo, UnableToDecryptReason, }; use ruma::{ events::{ @@ -250,27 +250,23 @@ where Raw::new(&self.construct_json(true)).unwrap().cast() } - pub fn into_timeline(self) -> TimelineEvent { - TimelineEvent::new(self.into_raw_timeline()) - } - pub fn into_raw_sync(self) -> Raw { Raw::new(&self.construct_json(false)).unwrap().cast() } - pub fn into_sync(self) -> SyncTimelineEvent { - SyncTimelineEvent::new(self.into_raw_sync()) + pub fn into_event(self) -> TimelineEvent { + TimelineEvent::new(self.into_raw_sync()) } } impl EventBuilder { - /// Turn this event into a SyncTimelineEvent representing a decryption + /// Turn this event into a [`TimelineEvent`] representing a decryption /// failure - pub fn into_utd_sync_timeline_event(self) -> SyncTimelineEvent { + pub fn into_utd_sync_timeline_event(self) -> TimelineEvent { let session_id = as_variant!(&self.content.scheme, EncryptedEventScheme::MegolmV1AesSha2) .map(|content| content.session_id.clone()); - SyncTimelineEvent::new_utd_event( + TimelineEvent::new_utd_event( self.into(), UnableToDecryptInfo { session_id, @@ -358,12 +354,12 @@ where } } -impl From> for SyncTimelineEvent +impl From> for TimelineEvent where E::EventType: Serialize, { fn from(val: EventBuilder) -> Self { - val.into_sync() + val.into_event() } }