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 <me@aloiseau.com>

Fixes #5598.
This commit is contained in:
Alexis Loiseau
2025-09-24 17:50:50 +02:00
committed by GitHub
parent 290f27a343
commit 0a0e31af83
12 changed files with 175 additions and 22 deletions

View File

@@ -380,6 +380,7 @@ pub enum MessageLikeEventType {
UnstablePollEnd,
UnstablePollResponse,
UnstablePollStart,
Other(String),
}
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
@@ -408,6 +409,7 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
MessageLikeEventType::Other(msgtype) => Self::from(msgtype),
}
}
}

View File

@@ -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<matrix_sdk_ui::timeline::MsgLikeContent> 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,
},
})
}
}

View File

@@ -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,
}),
}
}
})
}

View File

@@ -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},
};

View File

@@ -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",
}
}

View File

@@ -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
}
}

View File

@@ -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(_)

View File

@@ -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},

View File

@@ -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()

View File

@@ -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,

View File

@@ -123,6 +123,10 @@ fn format_timeline_item(item: &Arc<TimelineItem>, is_thread: bool) -> Option<Lis
kind: MsgLikeKind::Sticker(_),
..
})
| TimelineItemContent::MsgLike(MsgLikeContent {
kind: MsgLikeKind::Other(_),
..
})
| TimelineItemContent::ProfileChange(_)
| TimelineItemContent::OtherState(_)
| TimelineItemContent::FailedToParseMessageLike { .. }

View File

@@ -37,6 +37,7 @@ use ruma::{
call::{SessionDescription, invite::CallInviteEventContent},
direct::{DirectEventContent, OwnedDirectUserIdentifier},
ignored_user_list::IgnoredUserListEventContent,
macros::EventContent,
member_hints::MemberHintsEventContent,
poll::{
unstable_end::UnstablePollEndEventContent,
@@ -1076,6 +1077,11 @@ impl EventFactory {
event
}
/// Create a new `rs.matrix-sdk.custom.test` custom event
pub fn custom_message_like_event(&self) -> EventBuilder<CustomMessageLikeEventContent> {
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<MembershipState> for PreviousMembership {
Self::new(state)
}
}
#[derive(Clone, Default, Debug, Serialize, EventContent)]
#[ruma_event(type = "rs.matrix-sdk.custom.test", kind = MessageLike)]
pub struct CustomMessageLikeEventContent;