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::{
19    crypto::types::events::UtdCause,
20    deserialized_responses::{TimelineEvent, TimelineEventKind},
21    Room,
22};
23use ruma::{
24    events::{
25        poll::unstable_start::UnstablePollStartEventContent, AnyMessageLikeEventContent,
26        AnySyncTimelineEvent,
27    },
28    html::RemoveReplyFallback,
29    OwnedEventId, OwnedUserId, UserId,
30};
31use tracing::{debug, instrument, warn};
32
33use super::TimelineItemContent;
34use crate::timeline::{
35    event_item::{
36        content::{MsgLikeContent, MsgLikeKind},
37        extract_room_msg_edit_content, EventTimelineItem, Profile, TimelineDetails,
38    },
39    traits::RoomDataProvider,
40    EncryptedMessage, Error as TimelineError, Message, PollState, ReactionsByKeyBySender, Sticker,
41    TimelineItem,
42};
43
44/// Details about an event being replied to.
45#[derive(Clone, Debug)]
46pub struct InReplyToDetails {
47    /// The ID of the event.
48    pub event_id: OwnedEventId,
49
50    /// The details of the event.
51    ///
52    /// Use [`Timeline::fetch_details_for_event`] to fetch the data if it is
53    /// unavailable.
54    ///
55    /// [`Timeline::fetch_details_for_event`]: crate::Timeline::fetch_details_for_event
56    pub event: TimelineDetails<Box<RepliedToEvent>>,
57}
58
59impl InReplyToDetails {
60    pub fn new(
61        event_id: OwnedEventId,
62        timeline_items: &Vector<Arc<TimelineItem>>,
63    ) -> InReplyToDetails {
64        let event = timeline_items
65            .iter()
66            .filter_map(|it| it.as_event())
67            .find(|it| it.event_id() == Some(&*event_id))
68            .map(|item| Box::new(RepliedToEvent::from_timeline_item(item)));
69
70        InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
71    }
72}
73
74/// An event that is replied to.
75#[derive(Clone, Debug)]
76pub struct RepliedToEvent {
77    content: TimelineItemContent,
78    sender: OwnedUserId,
79    sender_profile: TimelineDetails<Profile>,
80}
81
82impl RepliedToEvent {
83    /// Get the message of this event.
84    pub fn content(&self) -> &TimelineItemContent {
85        &self.content
86    }
87
88    /// Get the sender of this event.
89    pub fn sender(&self) -> &UserId {
90        &self.sender
91    }
92
93    /// Get the profile of the sender.
94    pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
95        &self.sender_profile
96    }
97
98    /// Create a [`RepliedToEvent`] from a loaded event timeline item.
99    pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
100        Self {
101            content: timeline_item.content.clone(),
102            sender: timeline_item.sender.clone(),
103            sender_profile: timeline_item.sender_profile.clone(),
104        }
105    }
106
107    /// Try to create a `RepliedToEvent` from a `TimelineEvent` by providing the
108    /// room.
109    pub async fn try_from_timeline_event_for_room(
110        timeline_event: TimelineEvent,
111        room_data_provider: &Room,
112    ) -> Result<Self, TimelineError> {
113        Self::try_from_timeline_event(timeline_event, room_data_provider).await
114    }
115
116    #[instrument(skip_all)]
117    pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
118        timeline_event: TimelineEvent,
119        room_data_provider: &P,
120    ) -> Result<Self, TimelineError> {
121        let event = match timeline_event.raw().deserialize() {
122            Ok(AnySyncTimelineEvent::MessageLike(event)) => event,
123            Ok(_) => {
124                warn!("can't get details, event isn't a message-like event");
125                return Err(TimelineError::UnsupportedEvent);
126            }
127            Err(err) => {
128                warn!("can't get details, event couldn't be deserialized: {err}");
129                return Err(TimelineError::UnsupportedEvent);
130            }
131        };
132
133        debug!(event_type = %event.event_type(), "got deserialized event");
134
135        let content = match event.original_content() {
136            Some(content) => match content {
137                AnyMessageLikeEventContent::RoomMessage(c) => {
138                    // Assume we're not interested in reactions and thread info in this context:
139                    // this is information for an embedded (replied-to) event, that will usually not
140                    // include detailed information like reactions.
141                    let reactions = ReactionsByKeyBySender::default();
142                    let thread_root = None;
143
144                    TimelineItemContent::MsgLike(MsgLikeContent {
145                        kind: MsgLikeKind::Message(Message::from_event(
146                            c,
147                            extract_room_msg_edit_content(event.relations()),
148                            RemoveReplyFallback::Yes,
149                        )),
150                        reactions,
151                        thread_root,
152                        in_reply_to: None,
153                    })
154                }
155
156                AnyMessageLikeEventContent::Sticker(content) => {
157                    // Assume we're not interested in reactions or thread info in this context.
158                    // (See above an explanation as to why that's the case.)
159                    let reactions = ReactionsByKeyBySender::default();
160                    let thread_root = None;
161
162                    TimelineItemContent::MsgLike(MsgLikeContent {
163                        kind: MsgLikeKind::Sticker(Sticker { content }),
164                        reactions,
165                        thread_root,
166                        in_reply_to: None,
167                    })
168                }
169
170                AnyMessageLikeEventContent::RoomEncrypted(content) => {
171                    let utd_cause = match &timeline_event.kind {
172                        TimelineEventKind::UnableToDecrypt { utd_info, .. } => UtdCause::determine(
173                            timeline_event.raw(),
174                            room_data_provider.crypto_context_info().await,
175                            utd_info,
176                        ),
177                        _ => UtdCause::Unknown,
178                    };
179
180                    TimelineItemContent::MsgLike(MsgLikeContent::unable_to_decrypt(
181                        EncryptedMessage::from_content(content, utd_cause),
182                    ))
183                }
184
185                AnyMessageLikeEventContent::UnstablePollStart(
186                    UnstablePollStartEventContent::New(content),
187                ) => {
188                    // Assume we're not interested in reactions or thread info in this context.
189                    // (See above an explanation as to why that's the case.)
190                    let reactions = ReactionsByKeyBySender::default();
191                    let thread_root = None;
192
193                    // TODO: could we provide the bundled edit here?
194                    let poll_state = PollState::new(content, None);
195                    TimelineItemContent::MsgLike(MsgLikeContent {
196                        kind: MsgLikeKind::Poll(poll_state),
197                        reactions,
198                        thread_root,
199                        in_reply_to: None,
200                    })
201                }
202
203                _ => {
204                    warn!("unsupported event type");
205                    return Err(TimelineError::UnsupportedEvent);
206                }
207            },
208
209            None => TimelineItemContent::MsgLike(MsgLikeContent::redacted()),
210        };
211
212        let sender = event.sender().to_owned();
213        let sender_profile = TimelineDetails::from_initial_value(
214            room_data_provider.profile_from_user_id(&sender).await,
215        );
216
217        Ok(Self { content, sender, sender_profile })
218    }
219}