From 0a0e31af830d1069b28f885b6535b7d3e0da2f2f Mon Sep 17 00:00:00 2001 From: Alexis Loiseau <59391503+IT-ess@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:50:50 +0200 Subject: [PATCH] feat(ui): add custom events to timeline when explicitly filtered This allows custom message-like events (created by the `EventContent` macro from ruma) to be added to the timeline if they are explicitly allowed when building the timeline with a custom `event_filter`. The custom event content is not available directly to the consumer, but it can still fetch it from the matrix-sdk client with its `event_id`, or display a "this type of event is not supported". Signed-off-by: Itess Fixes #5598. --- bindings/matrix-sdk-ffi/src/event.rs | 2 + .../matrix-sdk-ffi/src/timeline/msg_like.rs | 13 ++++ .../src/timeline/event_handler.rs | 21 ++++--- .../src/timeline/event_item/content/mod.rs | 2 + .../timeline/event_item/content/msg_like.rs | 8 ++- .../src/timeline/event_item/content/other.rs | 35 +++++++++++ .../src/timeline/event_item/mod.rs | 9 +-- crates/matrix-sdk-ui/src/timeline/mod.rs | 8 +-- .../src/timeline/tests/event_filter.rs | 26 ++++++++ .../tests/integration/timeline/mod.rs | 59 +++++++++++++++++-- .../src/widgets/room_view/timeline.rs | 4 ++ testing/matrix-sdk-test/src/event_factory.rs | 10 ++++ 12 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 crates/matrix-sdk-ui/src/timeline/event_item/content/other.rs diff --git a/bindings/matrix-sdk-ffi/src/event.rs b/bindings/matrix-sdk-ffi/src/event.rs index a5b22f8c0..6f5f6d40e 100644 --- a/bindings/matrix-sdk-ffi/src/event.rs +++ b/bindings/matrix-sdk-ffi/src/event.rs @@ -380,6 +380,7 @@ pub enum MessageLikeEventType { UnstablePollEnd, UnstablePollResponse, UnstablePollStart, + Other(String), } impl From for ruma::events::MessageLikeEventType { @@ -408,6 +409,7 @@ impl From for ruma::events::MessageLikeEventType { MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd, MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse, MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart, + MessageLikeEventType::Other(msgtype) => Self::from(msgtype), } } } diff --git a/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs b/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs index 7b7536541..8283b20a2 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/msg_like.rs @@ -23,6 +23,7 @@ use super::{ }; use crate::{ error::ClientError, + event::MessageLikeEventType, ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind}, timeline::content::ReactionSenderData, utils::Timestamp, @@ -50,6 +51,9 @@ pub enum MsgLikeKind { /// An `m.room.encrypted` event that could not be decrypted. UnableToDecrypt { msg: EncryptedMessage }, + + /// A custom message like event. + Other { event_type: MessageLikeEventType }, } /// A special kind of [`super::TimelineItemContent`] that groups together @@ -182,6 +186,15 @@ impl TryFrom for MsgLikeContent { thread_root, thread_summary, }, + Kind::Other(other) => Self { + kind: MsgLikeKind::Other { + event_type: MessageLikeEventType::Other(other.event_type().to_string()), + }, + reactions, + in_reply_to, + thread_root, + thread_summary, + }, }) } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_handler.rs b/crates/matrix-sdk-ui/src/timeline/event_handler.rs index bfd959d59..ef92d074a 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_handler.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_handler.rs @@ -58,7 +58,8 @@ use super::{ traits::RoomDataProvider, }; use crate::{ - timeline::controller::aggregations::PendingEdit, unable_to_decrypt_hook::UtdHookManager, + timeline::{controller::aggregations::PendingEdit, event_item::OtherMessageLike}, + unable_to_decrypt_hook::UtdHookManager, }; /// When adding an event, useful information related to the source of the event. @@ -381,12 +382,18 @@ impl TimelineAction { ), }, - _ => { - debug!( - "Ignoring message-like event of type `{}`, not supported (yet)", - content.event_type() - ); - return None; + event => { + let other = OtherMessageLike { event_type: event.event_type() }; + + Self::AddItem { + content: TimelineItemContent::MsgLike(MsgLikeContent { + kind: MsgLikeKind::Other(other), + reactions: Default::default(), + thread_root, + in_reply_to, + thread_summary, + }), + } } }) } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs index 9bc428662..3cfc88d32 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/mod.rs @@ -62,6 +62,7 @@ use tracing::warn; mod message; mod msg_like; +pub(super) mod other; pub(crate) mod pinned_events; mod polls; mod reply; @@ -74,6 +75,7 @@ pub(in crate::timeline) use self::message::{ pub use self::{ message::Message, msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary}, + other::OtherMessageLike, polls::{PollResult, PollState}, reply::{EmbeddedEvent, InReplyToDetails}, }; diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs index d980ffed1..2abec19b6 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/msg_like.rs @@ -16,7 +16,9 @@ use as_variant::as_variant; use ruma::OwnedEventId; use super::{EmbeddedEvent, EncryptedMessage, InReplyToDetails, Message, PollState, Sticker}; -use crate::timeline::{ReactionsByKeyBySender, TimelineDetails}; +use crate::timeline::{ + ReactionsByKeyBySender, TimelineDetails, event_item::content::other::OtherMessageLike, +}; #[derive(Clone, Debug)] pub enum MsgLikeKind { @@ -34,6 +36,9 @@ pub enum MsgLikeKind { /// An `m.room.encrypted` event that could not be decrypted. UnableToDecrypt(EncryptedMessage), + + /// A custom message like event. + Other(OtherMessageLike), } #[derive(Clone, Debug)] @@ -74,6 +79,7 @@ impl MsgLikeContent { MsgLikeKind::Poll(_) => "a poll", MsgLikeKind::Redacted => "a redacted message", MsgLikeKind::UnableToDecrypt(_) => "an encrypted message we couldn't decrypt", + MsgLikeKind::Other(_) => "a custom message-like event", } } diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/other.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/other.rs new file mode 100644 index 000000000..20ba3aaa0 --- /dev/null +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/other.rs @@ -0,0 +1,35 @@ +// Copyright 2025 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. + +//! Timeline item content for other message-like events created by the +//! EventContent macro from ruma. + +use ruma::events::MessageLikeEventType; + +/// A custom event created by the EventContent macro from ruma. +#[derive(Debug, Clone, PartialEq)] +pub struct OtherMessageLike { + pub(in crate::timeline) event_type: MessageLikeEventType, +} + +impl OtherMessageLike { + pub fn from_event_type(event_type: MessageLikeEventType) -> Self { + Self { event_type } + } + + /// Get the event_type of this message. + pub fn event_type(&self) -> &MessageLikeEventType { + &self.event_type + } +} 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 6769790a8..1b172078e 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -46,9 +46,9 @@ mod remote; pub use self::{ content::{ AnyOtherFullStateEventContent, EmbeddedEvent, EncryptedMessage, InReplyToDetails, - MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherState, - PollResult, PollState, RoomMembershipChange, RoomPinnedEventsChange, Sticker, - ThreadSummary, TimelineItemContent, + MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, + OtherMessageLike, OtherState, PollResult, PollState, RoomMembershipChange, + RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent, }, local::{EventSendState, MediaUploadProgress}, }; @@ -608,7 +608,8 @@ impl EventTimelineItem { MsgLikeKind::Sticker(_) | MsgLikeKind::Poll(_) | MsgLikeKind::Redacted - | MsgLikeKind::UnableToDecrypt(_) => None, + | MsgLikeKind::UnableToDecrypt(_) + | MsgLikeKind::Other(_) => None, }, TimelineItemContent::MembershipChange(_) | TimelineItemContent::ProfileChange(_) diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index 6fd5ad365..6ddf12f64 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -95,10 +95,10 @@ pub use self::{ event_item::{ AnyOtherFullStateEventContent, EmbeddedEvent, EncryptedMessage, EventItemOrigin, EventSendState, EventTimelineItem, InReplyToDetails, MediaUploadProgress, - MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, OtherState, - PollResult, PollState, Profile, ReactionInfo, ReactionStatus, ReactionsByKeyBySender, - RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineDetails, - TimelineEventItemId, TimelineItemContent, + MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind, + OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus, + ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange, Sticker, + ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineItemContent, }, event_type_filter::TimelineEventTypeFilter, item::{TimelineItem, TimelineItemKind, TimelineUniqueId}, 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 c6dd0e37a..e7fef6b30 100644 --- a/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs +++ b/crates/matrix-sdk-ui/src/timeline/tests/event_filter.rs @@ -14,6 +14,7 @@ use std::sync::Arc; +use assert_matches::assert_matches; use assert_matches2::assert_let; use eyeball_im::VectorDiff; use matrix_sdk::deserialized_responses::TimelineEvent; @@ -137,6 +138,31 @@ async fn test_custom_filter() { assert_eq!(timeline.controller.items().await.len(), 3); } +#[async_test] +async fn test_custom_filter_for_custom_msglike_event() { + // Filter out all state events. + let timeline = TestTimelineBuilder::new() + .settings(TimelineSettings { + event_filter: Arc::new(|ev, _| matches!(ev, AnySyncTimelineEvent::MessageLike(_))), + ..Default::default() + }) + .build(); + let mut stream = timeline.subscribe().await; + + let f = &timeline.factory; + timeline.handle_live_event(f.custom_message_like_event().sender(&ALICE)).await; + let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value); + let date_divider = assert_next_matches!(stream, VectorDiff::PushFront { value } => value); + + assert_matches!( + item.as_event().unwrap().content().as_msglike().unwrap().kind.clone(), + MsgLikeKind::Other(_) + ); + assert!(date_divider.is_date_divider()); + + assert_eq!(timeline.controller.items().await.len(), 2); +} + #[async_test] async fn test_hide_failed_to_parse() { let timeline = TestTimelineBuilder::new() diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs index 455f664a8..19d18604b 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/mod.rs @@ -29,16 +29,19 @@ use matrix_sdk_test::{ use matrix_sdk_ui::{ Timeline, timeline::{ - AnyOtherFullStateEventContent, Error, EventSendState, RedactError, RoomExt, - TimelineBuilder, TimelineEventItemId, TimelineFocus, TimelineItemContent, - VirtualTimelineItem, + AnyOtherFullStateEventContent, Error, EventSendState, MsgLikeKind, OtherMessageLike, + RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineFocus, + TimelineItemContent, VirtualTimelineItem, default_event_filter, }, }; use ruma::{ EventId, MilliSecondsSinceUnixEpoch, event_id, - events::room::{ - encryption::RoomEncryptionEventContent, - message::{RedactedRoomMessageEventContent, RoomMessageEventContent}, + events::{ + MessageLikeEventType, TimelineEventType, + room::{ + encryption::RoomEncryptionEventContent, + message::{RedactedRoomMessageEventContent, RoomMessageEventContent}, + }, }, owned_event_id, room_id, user_id, }; @@ -884,6 +887,50 @@ async fn test_timeline_receives_a_limited_number_of_events_when_subscribing() { assert_pending!(timeline_stream); } +#[async_test] +async fn test_custom_msglike_event_in_timeline() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!a98sd12bjh:example.org"); + let room = server.sync_joined_room(&client, room_id).await; + + server.mock_room_state_encryption().plain().mount().await; + + let timeline = room + .timeline_builder() + .event_filter(|event, room_version| { + event.event_type() == TimelineEventType::from("rs.matrix-sdk.custom.test") + || default_event_filter(event, room_version) + }) + .build() + .await + .unwrap(); + let (_, mut timeline_stream) = timeline.subscribe().await; + + let event_id = event_id!("$eeG0HA0FAZ37wP8kXlNk123I"); + let f = EventFactory::new(); + server + .sync_room( + &client, + JoinedRoomBuilder::new(room_id).add_timeline_event( + f.custom_message_like_event().event_id(event_id).sender(user_id!("@a:b.c")), + ), + ) + .await; + + assert_let!(Some(timeline_updates) = timeline_stream.next().await); + assert_let!(VectorDiff::PushBack { value: first } = &timeline_updates[0]); + let event_type = MessageLikeEventType::from("rs.matrix-sdk.custom.test"); + let other_msglike = OtherMessageLike::from_event_type(event_type); + assert_matches!( + first.as_event().unwrap().content().as_msglike().unwrap().kind.clone(), + MsgLikeKind::Other(observed_other) => { + assert_eq!(observed_other, other_msglike); + } + ); +} + struct PinningTestSetup<'a> { event_id: &'a EventId, room_id: &'a ruma::RoomId, diff --git a/labs/multiverse/src/widgets/room_view/timeline.rs b/labs/multiverse/src/widgets/room_view/timeline.rs index 91e45addf..0066d913d 100644 --- a/labs/multiverse/src/widgets/room_view/timeline.rs +++ b/labs/multiverse/src/widgets/room_view/timeline.rs @@ -123,6 +123,10 @@ fn format_timeline_item(item: &Arc, is_thread: bool) -> Option EventBuilder { + self.event(CustomMessageLikeEventContent) + } + /// Set the next server timestamp. /// /// Timestamps will continue to increase by 1 (millisecond) from that value. @@ -1297,3 +1303,7 @@ impl From for PreviousMembership { Self::new(state) } } + +#[derive(Clone, Default, Debug, Serialize, EventContent)] +#[ruma_event(type = "rs.matrix-sdk.custom.test", kind = MessageLike)] +pub struct CustomMessageLikeEventContent;