matrix_sdk_ui/timeline/event_filter.rs
1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ruma::events::{
16 AnySyncStateEvent, AnySyncTimelineEvent, SyncStateEvent, TimelineEventType,
17 room::member::{MembershipChange, MembershipState},
18};
19
20/// A timeline filter that in- or excludes events based on their type or
21/// content.
22#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
23pub enum TimelineEventFilter {
24 /// Only return items whose event matches any of the conditions in the list.
25 Include(Vec<TimelineEventCondition>),
26 /// Return all items except the ones whose event matches any of the
27 /// conditions in the list
28 Exclude(Vec<TimelineEventCondition>),
29}
30
31impl TimelineEventFilter {
32 /// Filters any incoming `event` using the filter conditions.
33 ///
34 /// # Arguments
35 ///
36 /// * `event` - The event to run the filter on.
37 ///
38 /// # Returns
39 /// `true` if the filter allows the event or `false` otherwise.
40 pub fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
41 match self {
42 Self::Include(conditions) => conditions.iter().any(|c| c.matches(event)),
43 Self::Exclude(conditions) => !conditions.iter().any(|c| c.matches(event)),
44 }
45 }
46}
47
48/// A condition that matches on an event's type or content.
49#[derive(Clone)]
50#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
51pub enum TimelineEventCondition {
52 /// The event has the specified event type.
53 EventType(TimelineEventType),
54 /// The event is an `m.room.member` event that represents a membership
55 /// change (join, leave, etc.).
56 MembershipChange(MembershipChangeFilter),
57 /// The event is an `m.room.member` event that represents a profile
58 /// change (displayname or avatar URL).
59 ProfileChange,
60 /// The event is a custom message-like event type.
61 AnyCustomMessageLikeEvent,
62 /// The event is a custom state event type.
63 AnyCustomStateEvent,
64}
65
66/// The membership states that should be included/excluded from the timeline
67/// item filters.
68#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
69#[derive(Clone)]
70pub enum MembershipChangeFilter {
71 /// Include/exclude all membership state events.
72 Any,
73 /// Include/exclude only `join` membership state events.
74 Join,
75 /// Include/exclude only `leave` membership state events.
76 Leave,
77 /// Include/exclude only `invite` membership state events.
78 Invite,
79 /// Include/exclude only `ban` membership state events.
80 Ban,
81 /// Include/exclude only `knock` membership state events.
82 Knock,
83}
84
85impl TimelineEventCondition {
86 /// Evaluate the condition against an event.
87 ///
88 /// # Arguments
89 ///
90 /// * `event` - The event to test the condition against.
91 ///
92 /// # Returns
93 /// `true` if the condition matches or `false` otherwise.
94 fn matches(&self, event: &AnySyncTimelineEvent) -> bool {
95 match self {
96 Self::EventType(event_type) => event.event_type() == *event_type,
97 Self::MembershipChange(filter) => match event {
98 AnySyncTimelineEvent::State(AnySyncStateEvent::RoomMember(
99 SyncStateEvent::Original(ev),
100 )) => {
101 if matches!(ev.membership_change(), MembershipChange::ProfileChanged { .. }) {
102 return false;
103 }
104 match (filter, &ev.content.membership) {
105 (MembershipChangeFilter::Any, _) => {
106 !matches!(ev.membership_change(), MembershipChange::None)
107 }
108 (MembershipChangeFilter::Join, MembershipState::Join) => true,
109 (MembershipChangeFilter::Invite, MembershipState::Invite) => true,
110 (MembershipChangeFilter::Leave, MembershipState::Leave) => true,
111 (MembershipChangeFilter::Knock, MembershipState::Knock) => true,
112 (MembershipChangeFilter::Ban, MembershipState::Ban) => true,
113 _ => false,
114 }
115 }
116 _ => false,
117 },
118 Self::ProfileChange => match event {
119 AnySyncTimelineEvent::State(AnySyncStateEvent::RoomMember(
120 SyncStateEvent::Original(ev),
121 )) => {
122 matches!(ev.membership_change(), MembershipChange::ProfileChanged { .. })
123 }
124 _ => false,
125 },
126 Self::AnyCustomMessageLikeEvent => match event {
127 AnySyncTimelineEvent::MessageLike(_) => {
128 matches!(event.event_type(), TimelineEventType::_Custom(_))
129 }
130 AnySyncTimelineEvent::State(_) => false,
131 },
132 Self::AnyCustomStateEvent => match event {
133 AnySyncTimelineEvent::State(_) => {
134 matches!(event.event_type(), TimelineEventType::_Custom(_))
135 }
136 AnySyncTimelineEvent::MessageLike(_) => false,
137 },
138 }
139 }
140}