| 1 | use std::sync::Arc; |
| 2 | |
| 3 | use matrix_sdk_ui::{ |
| 4 | eyeball_im::VectorDiff, |
| 5 | timeline::{ |
| 6 | EventSendState, MsgLikeKind, TimelineDetails, TimelineEventItemId, |
| 7 | TimelineItem as SdkTimelineItem, TimelineItemContent, TimelineItemKind, |
| 8 | VirtualTimelineItem, |
| 9 | }, |
| 10 | }; |
| 11 | use ruma::{ |
| 12 | UserId, |
| 13 | events::room::message::{MessageFormat, MessageType}, |
| 14 | }; |
| 15 | |
| 16 | use crate::{ |
| 17 | MessageMentions, Reaction, ReplyReference, SendState, ThreadSummary, TimelineDiff, |
| 18 | TimelineItem, TimelineItemId, TimelineMessage, |
| 19 | }; |
| 20 | |
| 21 | use super::media::{ |
| 22 | MediaRegistration, MediaScope, media_descriptor_for_sticker, media_from_message, |
| 23 | media_identity, media_registration_for_item, |
| 24 | }; |
| 25 | |
| 26 | pub(crate) struct NormalisedTimeline { |
| 27 | pub(crate) items: Vec<TimelineItem>, |
| 28 | pub(crate) media: Vec<MediaRegistration>, |
| 29 | } |
| 30 | |
| 31 | pub(crate) fn normalise_timeline<'a>( |
| 32 | items: impl Iterator<Item = &'a Arc<SdkTimelineItem>>, |
| 33 | current_user_id: Option<&UserId>, |
| 34 | scope: &MediaScope, |
| 35 | ) -> NormalisedTimeline { |
| 36 | let mut normalised = NormalisedTimeline { |
| 37 | items: Vec::new(), |
| 38 | media: Vec::new(), |
| 39 | }; |
| 40 | for item in items { |
| 41 | if let Some(media) = media_registration_for_item(item, scope) { |
| 42 | normalised.media.push(media); |
| 43 | } |
| 44 | normalised |
| 45 | .items |
| 46 | .push(normalise_timeline_item(item, current_user_id, scope)); |
| 47 | } |
| 48 | normalised |
| 49 | } |
| 50 | |
| 51 | pub(crate) fn normalise_timeline_diffs( |
| 52 | diffs: Vec<VectorDiff<Arc<SdkTimelineItem>>>, |
| 53 | current_user_id: Option<&UserId>, |
| 54 | scope: &MediaScope, |
| 55 | ) -> NormalisedTimelineDiffs { |
| 56 | let mut media = Vec::new(); |
| 57 | let diffs = diffs |
| 58 | .into_iter() |
| 59 | .map(|diff| { |
| 60 | let diff = diff.map(|item| { |
| 61 | if let Some(registration) = media_registration_for_item(&item, scope) { |
| 62 | media.push(registration); |
| 63 | } |
| 64 | normalise_timeline_item(&item, current_user_id, scope) |
| 65 | }); |
| 66 | match diff { |
| 67 | VectorDiff::Append { values } => TimelineDiff::Append { |
| 68 | values: values.into_iter().collect(), |
| 69 | }, |
| 70 | VectorDiff::Clear => TimelineDiff::Clear, |
| 71 | VectorDiff::PushFront { value } => TimelineDiff::PushFront { value }, |
| 72 | VectorDiff::PushBack { value } => TimelineDiff::PushBack { value }, |
| 73 | VectorDiff::PopFront => TimelineDiff::PopFront, |
| 74 | VectorDiff::PopBack => TimelineDiff::PopBack, |
| 75 | VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value }, |
| 76 | VectorDiff::Set { index, value } => TimelineDiff::Set { index, value }, |
| 77 | VectorDiff::Remove { index } => TimelineDiff::Remove { index }, |
| 78 | VectorDiff::Truncate { length } => TimelineDiff::Truncate { length }, |
| 79 | VectorDiff::Reset { values } => TimelineDiff::Reset { |
| 80 | values: values.into_iter().collect(), |
| 81 | }, |
| 82 | } |
| 83 | }) |
| 84 | .collect(); |
| 85 | NormalisedTimelineDiffs { diffs, media } |
| 86 | } |
| 87 | |
| 88 | pub(crate) struct NormalisedTimelineDiffs { |
| 89 | pub(crate) diffs: Vec<TimelineDiff>, |
| 90 | pub(crate) media: Vec<MediaRegistration>, |
| 91 | } |
| 92 | |
| 93 | fn normalise_timeline_item( |
| 94 | item: &SdkTimelineItem, |
| 95 | current_user_id: Option<&UserId>, |
| 96 | scope: &MediaScope, |
| 97 | ) -> TimelineItem { |
| 98 | match item.kind() { |
| 99 | TimelineItemKind::Event(event) => TimelineItem::Message(Box::new(timeline_message( |
| 100 | item, |
| 101 | event, |
| 102 | current_user_id, |
| 103 | scope, |
| 104 | ))), |
| 105 | TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => { |
| 106 | TimelineItem::DayDivider { |
| 107 | timestamp_ms: timestamp.get().into(), |
| 108 | } |
| 109 | } |
| 110 | TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker, |
| 111 | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => { |
| 112 | TimelineItem::TimelineStart |
| 113 | } |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | fn timeline_message( |
| 118 | item: &SdkTimelineItem, |
| 119 | event: &matrix_sdk_ui::timeline::EventTimelineItem, |
| 120 | current_user_id: Option<&UserId>, |
| 121 | scope: &MediaScope, |
| 122 | ) -> TimelineMessage { |
| 123 | let (body, formatted_body, mentions, edited, unable_to_decrypt, media) = match event.content() { |
| 124 | TimelineItemContent::MsgLike(content) => match &content.kind { |
| 125 | MsgLikeKind::Message(message) => ( |
| 126 | message.body().to_owned(), |
| 127 | formatted_body(message.msgtype()), |
| 128 | message.mentions().map(message_mentions), |
| 129 | message.is_edited(), |
| 130 | false, |
| 131 | media_from_message(&media_identity(item, scope), message.msgtype()), |
| 132 | ), |
| 133 | MsgLikeKind::Sticker(sticker) => { |
| 134 | let content = sticker.content(); |
| 135 | ( |
| 136 | content.body.clone(), |
| 137 | None, |
| 138 | None, |
| 139 | false, |
| 140 | false, |
| 141 | Some(media_descriptor_for_sticker( |
| 142 | &media_identity(item, scope), |
| 143 | content, |
| 144 | )), |
| 145 | ) |
| 146 | } |
| 147 | MsgLikeKind::Redacted => ("<message redacted>".into(), None, None, false, false, None), |
| 148 | MsgLikeKind::UnableToDecrypt(_) => ( |
| 149 | "<unable to decrypt message>".into(), |
| 150 | None, |
| 151 | None, |
| 152 | false, |
| 153 | true, |
| 154 | None, |
| 155 | ), |
| 156 | MsgLikeKind::Poll(_) => ("[poll]".into(), None, None, false, false, None), |
| 157 | MsgLikeKind::LiveLocation(_) => { |
| 158 | ("[live location]".into(), None, None, false, false, None) |
| 159 | } |
| 160 | MsgLikeKind::Other(other) => ( |
| 161 | format!("[{}]", other.event_type()), |
| 162 | None, |
| 163 | None, |
| 164 | false, |
| 165 | false, |
| 166 | None, |
| 167 | ), |
| 168 | }, |
| 169 | other => ( |
| 170 | format!( |
| 171 | "[{}]", |
| 172 | other.event_type_str().unwrap_or_else(|| "event".into()) |
| 173 | ), |
| 174 | None, |
| 175 | None, |
| 176 | false, |
| 177 | false, |
| 178 | None, |
| 179 | ), |
| 180 | }; |
| 181 | let reply_to = match event.content() { |
| 182 | TimelineItemContent::MsgLike(content) => content.in_reply_to.as_ref().map(|reply| { |
| 183 | let event_id = reply.event_id.to_owned(); |
| 184 | let embedded_sender = match &reply.event { |
| 185 | TimelineDetails::Ready(event) => Some(( |
| 186 | event.sender.clone(), |
| 187 | sender_name(&event.sender, &event.sender_profile), |
| 188 | )), |
| 189 | _ => None, |
| 190 | }; |
| 191 | ReplyReference { |
| 192 | event_id, |
| 193 | sender_id: embedded_sender.as_ref().map(|(id, _)| id.clone()), |
| 194 | sender_name: embedded_sender.map(|(_, name)| name), |
| 195 | } |
| 196 | }), |
| 197 | _ => None, |
| 198 | }; |
| 199 | let (thread_root, thread_summary) = match event.content() { |
| 200 | TimelineItemContent::MsgLike(content) => ( |
| 201 | content.thread_root.clone(), |
| 202 | content.thread_summary.as_ref().map(|summary| { |
| 203 | let latest_event = match &summary.latest_event { |
| 204 | TimelineDetails::Ready(event) => Some(event.as_ref()), |
| 205 | _ => None, |
| 206 | }; |
| 207 | ThreadSummary { |
| 208 | reply_count: summary.num_replies, |
| 209 | latest_event_id: latest_event.and_then(|event| match &event.identifier { |
| 210 | TimelineEventItemId::EventId(event_id) => Some(event_id.clone()), |
| 211 | TimelineEventItemId::TransactionId(_) => None, |
| 212 | }), |
| 213 | latest_event_timestamp_ms: latest_event |
| 214 | .map(|event| event.timestamp.get().into()), |
| 215 | } |
| 216 | }), |
| 217 | ), |
| 218 | _ => (None, None), |
| 219 | }; |
| 220 | let reactions = event |
| 221 | .content() |
| 222 | .reactions() |
| 223 | .into_iter() |
| 224 | .flat_map(|reactions| reactions.iter()) |
| 225 | .map(|(key, senders)| Reaction { |
| 226 | key: key.clone(), |
| 227 | count: senders.len(), |
| 228 | own: current_user_id.is_some_and(|user_id| senders.contains_key(user_id)), |
| 229 | senders: senders.keys().cloned().collect(), |
| 230 | }) |
| 231 | .collect(); |
| 232 | let mut read_by: Vec<_> = event |
| 233 | .read_receipts() |
| 234 | .keys() |
| 235 | .filter(|user_id| *user_id != event.sender()) |
| 236 | .cloned() |
| 237 | .collect(); |
| 238 | read_by.sort(); |
| 239 | |
| 240 | TimelineMessage { |
| 241 | item_id: TimelineItemId::new(item.unique_id().0.clone()), |
| 242 | event_id: event.event_id().map(ToOwned::to_owned), |
| 243 | transaction_id: event.transaction_id().map(ToOwned::to_owned), |
| 244 | sender_id: event.sender().to_owned(), |
| 245 | sender_name: sender_name(event.sender(), event.sender_profile()), |
| 246 | body, |
| 247 | formatted_body, |
| 248 | mentions, |
| 249 | thread_root, |
| 250 | thread_summary, |
| 251 | timestamp_ms: event.timestamp().get().into(), |
| 252 | own: event.is_own(), |
| 253 | send_state: match event.send_state() { |
| 254 | None | Some(EventSendState::Sent { .. }) => SendState::Sent, |
| 255 | Some(EventSendState::NotSentYet { progress: Some(_) }) => SendState::Sending, |
| 256 | Some(EventSendState::NotSentYet { progress: None }) => SendState::Queued, |
| 257 | Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed { |
| 258 | retriable: *is_recoverable, |
| 259 | }, |
| 260 | }, |
| 261 | edited, |
| 262 | unable_to_decrypt, |
| 263 | media, |
| 264 | reply_to, |
| 265 | reactions, |
| 266 | read_by, |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | fn sender_name( |
| 271 | sender: &UserId, |
| 272 | sender_profile: &TimelineDetails<matrix_sdk_ui::timeline::Profile>, |
| 273 | ) -> String { |
| 274 | match sender_profile { |
| 275 | TimelineDetails::Ready(profile) => profile |
| 276 | .display_name |
| 277 | .clone() |
| 278 | .unwrap_or_else(|| sender.to_string()), |
| 279 | _ => sender.to_string(), |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | fn formatted_body(message: &MessageType) -> Option<String> { |
| 284 | let formatted = match message { |
| 285 | MessageType::Text(content) => content.formatted.as_ref(), |
| 286 | MessageType::Emote(content) => content.formatted.as_ref(), |
| 287 | MessageType::Notice(content) => content.formatted.as_ref(), |
| 288 | MessageType::Audio(content) => content.formatted_caption(), |
| 289 | MessageType::File(content) => content.formatted_caption(), |
| 290 | MessageType::Image(content) => content.formatted_caption(), |
| 291 | MessageType::Video(content) => content.formatted_caption(), |
| 292 | _ => None, |
| 293 | }?; |
| 294 | (formatted.format == MessageFormat::Html).then(|| formatted.body.clone()) |
| 295 | } |
| 296 | |
| 297 | fn message_mentions(mentions: &ruma::events::Mentions) -> MessageMentions { |
| 298 | MessageMentions { |
| 299 | user_ids: mentions.user_ids.iter().cloned().collect(), |
| 300 | room: mentions.room, |
| 301 | } |
| 302 | } |