matrix_sdk_ui/timeline/event_item/content/
reply.rs

1// Copyright 2024 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::sync::Arc;
16
17use imbl::Vector;
18use matrix_sdk::deserialized_responses::TimelineEvent;
19use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId};
20use tracing::{debug, instrument, warn};
21
22use super::TimelineItemContent;
23use crate::timeline::{
24    Error as TimelineError, MsgLikeContent, MsgLikeKind, PollState, TimelineEventItemId,
25    TimelineItem,
26    controller::TimelineMetadata,
27    event_handler::{HandleAggregationKind, TimelineAction},
28    event_item::{EventTimelineItem, Profile, TimelineDetails},
29    traits::RoomDataProvider,
30};
31
32/// Details about an event being replied to.
33#[derive(Clone, Debug)]
34pub struct InReplyToDetails {
35    /// The ID of the event.
36    pub event_id: OwnedEventId,
37
38    /// The details of the event.
39    ///
40    /// Use [`Timeline::fetch_details_for_event`] to fetch the data if it is
41    /// unavailable.
42    ///
43    /// [`Timeline::fetch_details_for_event`]: crate::Timeline::fetch_details_for_event
44    pub event: TimelineDetails<Box<EmbeddedEvent>>,
45}
46
47impl InReplyToDetails {
48    pub fn new(
49        event_id: OwnedEventId,
50        timeline_items: &Vector<Arc<TimelineItem>>,
51    ) -> InReplyToDetails {
52        let event = timeline_items
53            .iter()
54            .filter_map(|it| it.as_event())
55            .find(|it| it.event_id() == Some(&*event_id))
56            .map(|item| Box::new(EmbeddedEvent::from_timeline_item(item)));
57
58        InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
59    }
60}
61
62/// An event that is embedded in another event, such as a replied-to event, or a
63/// thread latest event.
64#[derive(Clone, Debug)]
65pub struct EmbeddedEvent {
66    /// The content of the embedded item.
67    pub content: TimelineItemContent,
68    /// The user ID of the sender of the related embedded event.
69    pub sender: OwnedUserId,
70    /// The profile of the sender of the related embedded event.
71    pub sender_profile: TimelineDetails<Profile>,
72    /// The timestamp of the event.
73    pub timestamp: MilliSecondsSinceUnixEpoch,
74    /// The unique identifier of this event.
75    ///
76    /// This is the transaction ID for a local echo that has not been sent and
77    /// the event ID for a local echo that has been sent or a remote event.
78    pub identifier: TimelineEventItemId,
79}
80
81impl EmbeddedEvent {
82    /// Create a [`EmbeddedEvent`] from a loaded event timeline item.
83    pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
84        Self {
85            content: timeline_item.content.clone(),
86            sender: timeline_item.sender.clone(),
87            sender_profile: timeline_item.sender_profile.clone(),
88            timestamp: timeline_item.timestamp,
89            identifier: timeline_item.identifier(),
90        }
91    }
92
93    #[instrument(skip_all)]
94    pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
95        timeline_event: TimelineEvent,
96        room_data_provider: &P,
97        meta: &TimelineMetadata,
98    ) -> Result<Option<Self>, TimelineError> {
99        let (raw_event, unable_to_decrypt_info) = match timeline_event.kind {
100            matrix_sdk::deserialized_responses::TimelineEventKind::UnableToDecrypt {
101                utd_info,
102                event,
103            } => (event, Some(utd_info)),
104            _ => (timeline_event.kind.into_raw(), None),
105        };
106
107        let event = match raw_event.deserialize() {
108            Ok(event) => event,
109            Err(err) => {
110                warn!("can't get details, event couldn't be deserialized: {err}");
111                return Err(TimelineError::UnsupportedEvent);
112            }
113        };
114
115        debug!(event_type = %event.event_type(), "got deserialized event");
116
117        // We don't need to fill relation information or process metadata for an
118        // embedded reply.
119        let in_reply_to = None;
120        let thread_root = None;
121        let thread_summary = None;
122
123        let sender = event.sender().to_owned();
124        let timestamp = event.origin_server_ts();
125        let identifier = TimelineEventItemId::EventId(event.event_id().to_owned());
126        let action = TimelineAction::from_event(
127            event,
128            &raw_event,
129            room_data_provider,
130            unable_to_decrypt_info.map(|utd_info| (utd_info, meta.unable_to_decrypt_hook.as_ref())),
131            in_reply_to,
132            thread_root,
133            thread_summary,
134        )
135        .await;
136
137        match action {
138            Some(TimelineAction::AddItem { content }) => {
139                let sender_profile = TimelineDetails::from_initial_value(
140                    room_data_provider.profile_from_user_id(&sender).await,
141                );
142                Ok(Some(Self { content, sender, sender_profile, timestamp, identifier }))
143            }
144
145            Some(TimelineAction::HandleAggregation { kind, .. }) => {
146                // As an exception, edits are allowed to be embedded events.
147
148                // For an embedded event, we don't need to fill a few fields; it's in an
149                // embedded view context, so there's no strong need to show all detailed
150                // information about it.
151                let reactions = Default::default();
152                let thread_root = None;
153                let in_reply_to = None;
154                let thread_summary = None;
155
156                let content = match kind {
157                    HandleAggregationKind::Edit { replacement } => {
158                        let msg = replacement.new_content;
159                        Some(TimelineItemContent::message(
160                            msg.msgtype,
161                            msg.mentions,
162                            reactions,
163                            thread_root,
164                            in_reply_to,
165                            thread_summary,
166                        ))
167                    }
168
169                    HandleAggregationKind::PollEdit { replacement } => {
170                        let msg = replacement.new_content;
171                        let poll_state = PollState::new(msg.poll_start, msg.text);
172                        Some(TimelineItemContent::MsgLike(MsgLikeContent {
173                            kind: MsgLikeKind::Poll(poll_state),
174                            reactions: Default::default(),
175                            thread_root,
176                            in_reply_to,
177                            thread_summary,
178                        }))
179                    }
180
181                    _ => {
182                        // The event can't be represented as a standalone timeline item.
183                        warn!("embedded event is an aggregation: {}", kind.debug_string());
184                        None
185                    }
186                };
187
188                if let Some(content) = content {
189                    let sender_profile = TimelineDetails::from_initial_value(
190                        room_data_provider.profile_from_user_id(&sender).await,
191                    );
192                    Ok(Some(Self { content, sender, sender_profile, timestamp, identifier }))
193                } else {
194                    Ok(None)
195                }
196            }
197
198            None => {
199                warn!("embedded event lead to no action (neither an aggregation nor a new item)");
200                Ok(None)
201            }
202        }
203    }
204}