matrix_sdk_ffi/timeline/
content.rs

1// Copyright 2023 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 std::collections::HashMap;
16
17use matrix_sdk::room::power_levels::power_level_user_changes;
18use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
19use ruma::events::{
20    room::history_visibility::HistoryVisibility as RumaHistoryVisibility, FullStateEventContent,
21};
22
23use crate::{client::JoinRule, timeline::msg_like::MsgLikeContent, utils::Timestamp};
24
25impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
26    fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
27        use matrix_sdk_ui::timeline::TimelineItemContent as Content;
28
29        match value {
30            Content::MsgLike(msg_like) => match msg_like.try_into() {
31                Ok(content) => TimelineItemContent::MsgLike { content },
32                Err((error, event_type)) => TimelineItemContent::FailedToParseMessageLike {
33                    event_type,
34                    error: error.to_string(),
35                },
36            },
37
38            Content::CallInvite => TimelineItemContent::CallInvite,
39
40            Content::RtcNotification => TimelineItemContent::RtcNotification,
41
42            Content::MembershipChange(membership) => {
43                let reason = match membership.content() {
44                    FullStateEventContent::Original { content, .. } => content.reason.clone(),
45                    _ => None,
46                };
47                TimelineItemContent::RoomMembership {
48                    user_id: membership.user_id().to_string(),
49                    user_display_name: membership.display_name(),
50                    change: membership.change().map(Into::into),
51                    reason,
52                }
53            }
54
55            Content::ProfileChange(profile) => {
56                let (display_name, prev_display_name) = profile
57                    .displayname_change()
58                    .map(|change| (change.new.clone(), change.old.clone()))
59                    .unzip();
60                let (avatar_url, prev_avatar_url) = profile
61                    .avatar_url_change()
62                    .map(|change| {
63                        (
64                            change.new.as_ref().map(ToString::to_string),
65                            change.old.as_ref().map(ToString::to_string),
66                        )
67                    })
68                    .unzip();
69                TimelineItemContent::ProfileChange {
70                    display_name: display_name.flatten(),
71                    prev_display_name: prev_display_name.flatten(),
72                    avatar_url: avatar_url.flatten(),
73                    prev_avatar_url: prev_avatar_url.flatten(),
74                }
75            }
76
77            Content::OtherState(state) => TimelineItemContent::State {
78                state_key: state.state_key().to_owned(),
79                content: state.content().into(),
80            },
81
82            Content::FailedToParseMessageLike { event_type, error } => {
83                TimelineItemContent::FailedToParseMessageLike {
84                    event_type: event_type.to_string(),
85                    error: error.to_string(),
86                }
87            }
88
89            Content::FailedToParseState { event_type, state_key, error } => {
90                TimelineItemContent::FailedToParseState {
91                    event_type: event_type.to_string(),
92                    state_key,
93                    error: error.to_string(),
94                }
95            }
96        }
97    }
98}
99
100#[derive(Debug, Clone, uniffi::Enum)]
101pub enum HistoryVisibility {
102    /// Previous events are accessible to newly joined members from the point
103    /// they were invited onwards.
104    ///
105    /// Events stop being accessible when the member' state changes to
106    /// something other than *invite* or *join*.
107    Invited,
108
109    /// Previous events are accessible to newly joined members from the point
110    /// they joined the room onwards.
111    /// Events stop being accessible when the member' state changes to
112    /// something other than *join*.
113    Joined,
114
115    /// Previous events are always accessible to newly joined members.
116    ///
117    /// All events in the room are accessible, even those sent when the member
118    /// was not a part of the room.
119    Shared,
120
121    /// All events while this is the `HistoryVisibility` value may be shared by
122    /// any participating homeserver with anyone, regardless of whether they
123    /// have ever joined the room.
124    WorldReadable,
125
126    /// A custom history visibility, up for interpretation by the consumer.
127    Custom {
128        /// The string representation for this custom history visibility.
129        repr: String,
130    },
131}
132
133impl From<RumaHistoryVisibility> for HistoryVisibility {
134    fn from(value: RumaHistoryVisibility) -> Self {
135        match value {
136            RumaHistoryVisibility::Invited => Self::Invited,
137            RumaHistoryVisibility::Joined => Self::Joined,
138            RumaHistoryVisibility::Shared => Self::Shared,
139            RumaHistoryVisibility::WorldReadable => Self::WorldReadable,
140            _ => Self::Custom { repr: value.to_string() },
141        }
142    }
143}
144
145#[derive(Clone, uniffi::Enum)]
146// A note about this `allow(clippy::large_enum_variant)`.
147// In order to reduce the size of `TimelineItemContent`, we would need to
148// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
149// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
150// `uniffi::Object`, which is a radical change. It would simplify the memory
151// usage, but it would slow down the performance around the FFI border. Thus,
152// let's consider this is a false-positive lint in this particular case.
153#[allow(clippy::large_enum_variant)]
154pub enum TimelineItemContent {
155    MsgLike {
156        content: MsgLikeContent,
157    },
158    CallInvite,
159    RtcNotification,
160    RoomMembership {
161        user_id: String,
162        user_display_name: Option<String>,
163        change: Option<MembershipChange>,
164        reason: Option<String>,
165    },
166    ProfileChange {
167        display_name: Option<String>,
168        prev_display_name: Option<String>,
169        avatar_url: Option<String>,
170        prev_avatar_url: Option<String>,
171    },
172    State {
173        state_key: String,
174        content: OtherState,
175    },
176    FailedToParseMessageLike {
177        event_type: String,
178        error: String,
179    },
180    FailedToParseState {
181        event_type: String,
182        state_key: String,
183        error: String,
184    },
185}
186
187#[derive(Clone, uniffi::Record)]
188pub struct Reaction {
189    pub key: String,
190    pub senders: Vec<ReactionSenderData>,
191}
192
193#[derive(Clone, uniffi::Record)]
194pub struct ReactionSenderData {
195    pub sender_id: String,
196    pub timestamp: Timestamp,
197}
198
199#[derive(Clone, uniffi::Enum)]
200pub enum MembershipChange {
201    None,
202    Error,
203    Joined,
204    Left,
205    Banned,
206    Unbanned,
207    Kicked,
208    Invited,
209    KickedAndBanned,
210    InvitationAccepted,
211    InvitationRejected,
212    InvitationRevoked,
213    Knocked,
214    KnockAccepted,
215    KnockRetracted,
216    KnockDenied,
217    NotImplemented,
218}
219
220impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
221    fn from(membership_change: matrix_sdk_ui::timeline::MembershipChange) -> Self {
222        use matrix_sdk_ui::timeline::MembershipChange as Change;
223        match membership_change {
224            Change::None => Self::None,
225            Change::Error => Self::Error,
226            Change::Joined => Self::Joined,
227            Change::Left => Self::Left,
228            Change::Banned => Self::Banned,
229            Change::Unbanned => Self::Unbanned,
230            Change::Kicked => Self::Kicked,
231            Change::Invited => Self::Invited,
232            Change::KickedAndBanned => Self::KickedAndBanned,
233            Change::InvitationAccepted => Self::InvitationAccepted,
234            Change::InvitationRejected => Self::InvitationRejected,
235            Change::InvitationRevoked => Self::InvitationRevoked,
236            Change::Knocked => Self::Knocked,
237            Change::KnockAccepted => Self::KnockAccepted,
238            Change::KnockRetracted => Self::KnockRetracted,
239            Change::KnockDenied => Self::KnockDenied,
240            Change::NotImplemented => Self::NotImplemented,
241        }
242    }
243}
244
245#[derive(Clone, uniffi::Enum)]
246pub enum OtherState {
247    PolicyRuleRoom,
248    PolicyRuleServer,
249    PolicyRuleUser,
250    RoomAliases,
251    RoomAvatar { url: Option<String> },
252    RoomCanonicalAlias,
253    RoomCreate { federate: Option<bool> },
254    RoomEncryption,
255    RoomGuestAccess,
256    RoomHistoryVisibility { history_visibility: Option<HistoryVisibility> },
257    RoomJoinRules { join_rule: Option<JoinRule> },
258    RoomName { name: Option<String> },
259    RoomPinnedEvents { change: RoomPinnedEventsChange },
260    RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
261    RoomServerAcl,
262    RoomThirdPartyInvite { display_name: Option<String> },
263    RoomTombstone,
264    RoomTopic { topic: Option<String> },
265    SpaceChild,
266    SpaceParent,
267    Custom { event_type: String },
268}
269
270impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
271    fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
272        use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
273        use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
274
275        match content {
276            Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
277            Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
278            Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
279            Content::RoomAliases(_) => Self::RoomAliases,
280            Content::RoomAvatar(c) => {
281                let url = match c {
282                    FullContent::Original { content, .. } => {
283                        content.url.as_ref().map(ToString::to_string)
284                    }
285                    FullContent::Redacted(_) => None,
286                };
287                Self::RoomAvatar { url }
288            }
289            Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
290            Content::RoomCreate(c) => {
291                let federate = match c {
292                    FullContent::Original { content, .. } => Some(content.federate),
293                    FullContent::Redacted(_) => None,
294                };
295                Self::RoomCreate { federate }
296            }
297            Content::RoomEncryption(_) => Self::RoomEncryption,
298            Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
299            Content::RoomHistoryVisibility(c) => {
300                let history_visibility = match c {
301                    FullContent::Original { content, .. } => {
302                        Some(content.history_visibility.clone().into())
303                    }
304                    FullContent::Redacted(_) => None,
305                };
306                Self::RoomHistoryVisibility { history_visibility }
307            }
308            Content::RoomJoinRules(c) => {
309                let join_rule = match c {
310                    FullContent::Original { content, .. } => {
311                        match content.join_rule.clone().try_into() {
312                            Ok(jr) => Some(jr),
313                            Err(err) => {
314                                tracing::error!("Failed to convert join rule: {}", err);
315                                None
316                            }
317                        }
318                    }
319                    FullContent::Redacted(_) => None,
320                };
321                Self::RoomJoinRules { join_rule }
322            }
323            Content::RoomName(c) => {
324                let name = match c {
325                    FullContent::Original { content, .. } => Some(content.name.clone()),
326                    FullContent::Redacted(_) => None,
327                };
328                Self::RoomName { name }
329            }
330            Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
331            Content::RoomPowerLevels(c) => match c {
332                FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
333                    users: power_level_user_changes(content, prev_content)
334                        .iter()
335                        .map(|(k, v)| (k.to_string(), *v))
336                        .collect(),
337                    previous: prev_content.as_ref().map(|prev_content| {
338                        prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
339                    }),
340                },
341                FullContent::Redacted(_) => {
342                    Self::RoomPowerLevels { users: Default::default(), previous: None }
343                }
344            },
345            Content::RoomServerAcl(_) => Self::RoomServerAcl,
346            Content::RoomThirdPartyInvite(c) => {
347                let display_name = match c {
348                    FullContent::Original { content, .. } => Some(content.display_name.clone()),
349                    FullContent::Redacted(_) => None,
350                };
351                Self::RoomThirdPartyInvite { display_name }
352            }
353            Content::RoomTombstone(_) => Self::RoomTombstone,
354            Content::RoomTopic(c) => {
355                let topic = match c {
356                    FullContent::Original { content, .. } => Some(content.topic.clone()),
357                    FullContent::Redacted(_) => None,
358                };
359                Self::RoomTopic { topic }
360            }
361            Content::SpaceChild(_) => Self::SpaceChild,
362            Content::SpaceParent(_) => Self::SpaceParent,
363            Content::_Custom { event_type, .. } => Self::Custom { event_type: event_type.clone() },
364        }
365    }
366}