| 1 | // Copyright 2025 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 that specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | use anyhow::{Context, bail}; |
| 16 | use matrix_sdk::IdParseError; |
| 17 | use matrix_sdk_ui::timeline::TimelineEventItemId; |
| 18 | use ruma::{ |
| 19 | EventId, |
| 20 | events::{ |
| 21 | AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, |
| 22 | MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent, |
| 23 | RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent, |
| 24 | TimelineEventType as RumaTimelineEventType, |
| 25 | room::{ |
| 26 | encrypted, |
| 27 | message::{MessageType as RumaMessageType, Relation}, |
| 28 | redaction::SyncRoomRedactionEvent, |
| 29 | }, |
| 30 | }, |
| 31 | }; |
| 32 | |
| 33 | use crate::{ |
| 34 | ClientError, |
| 35 | room_member::MembershipState, |
| 36 | ruma::{MessageType, RtcCallIntent, RtcNotificationType}, |
| 37 | utils::Timestamp, |
| 38 | }; |
| 39 | |
| 40 | #[derive(uniffi::Object)] |
| 41 | pub struct TimelineEvent(pub(crate) Box<AnySyncTimelineEvent>); |
| 42 | |
| 43 | #[matrix_sdk_ffi_macros::export] |
| 44 | impl TimelineEvent { |
| 45 | pub fn event_id(&self) -> String { |
| 46 | self.0.event_id().to_string() |
| 47 | } |
| 48 | |
| 49 | pub fn sender_id(&self) -> String { |
| 50 | self.0.sender().to_string() |
| 51 | } |
| 52 | |
| 53 | pub fn timestamp(&self) -> Timestamp { |
| 54 | self.0.origin_server_ts().into() |
| 55 | } |
| 56 | |
| 57 | pub fn content(&self) -> Result<TimelineEventContent, ClientError> { |
| 58 | let content = match &*self.0 { |
| 59 | AnySyncTimelineEvent::MessageLike(event) => { |
| 60 | TimelineEventContent::MessageLike { content: event.clone().try_into()? } |
| 61 | } |
| 62 | AnySyncTimelineEvent::State(event) => { |
| 63 | TimelineEventContent::State { content: event.clone().try_into()? } |
| 64 | } |
| 65 | }; |
| 66 | Ok(content) |
| 67 | } |
| 68 | |
| 69 | /// Returns the thread root event id for the event, if it's part of a |
| 70 | /// thread. |
| 71 | pub fn thread_root_event_id(&self) -> Option<String> { |
| 72 | match &*self.0 { |
| 73 | AnySyncTimelineEvent::MessageLike(event) => { |
| 74 | match event.original_content().and_then(|content| content.relation()) { |
| 75 | Some(encrypted::Relation::Thread(thread)) => Some(thread.event_id.to_string()), |
| 76 | _ => None, |
| 77 | } |
| 78 | } |
| 79 | AnySyncTimelineEvent::State(_) => None, |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | impl From<AnyTimelineEvent> for TimelineEvent { |
| 85 | fn from(event: AnyTimelineEvent) -> Self { |
| 86 | Self(Box::new(event.into())) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | /// The timeline event type. |
| 91 | #[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)] |
| 92 | #[uniffi::export(Eq, Hash)] |
| 93 | pub enum TimelineEventType { |
| 94 | /// The event is a message-like one and should be displayed as such. |
| 95 | MessageLike { value: MessageLikeEventType }, |
| 96 | /// The event is a state event, and may or may not be displayed in the |
| 97 | /// timeline. |
| 98 | State { value: StateEventType }, |
| 99 | } |
| 100 | |
| 101 | impl From<RumaTimelineEventType> for TimelineEventType { |
| 102 | fn from(value: RumaTimelineEventType) -> Self { |
| 103 | match value { |
| 104 | RumaTimelineEventType::Audio => { |
| 105 | Self::MessageLike { value: MessageLikeEventType::Audio } |
| 106 | } |
| 107 | RumaTimelineEventType::File => Self::MessageLike { value: MessageLikeEventType::File }, |
| 108 | RumaTimelineEventType::Image => { |
| 109 | Self::MessageLike { value: MessageLikeEventType::Image } |
| 110 | } |
| 111 | RumaTimelineEventType::Video => { |
| 112 | Self::MessageLike { value: MessageLikeEventType::Video } |
| 113 | } |
| 114 | RumaTimelineEventType::Voice => { |
| 115 | Self::MessageLike { value: MessageLikeEventType::Voice } |
| 116 | } |
| 117 | RumaTimelineEventType::Emote => { |
| 118 | Self::MessageLike { value: MessageLikeEventType::Emote } |
| 119 | } |
| 120 | RumaTimelineEventType::Encrypted => { |
| 121 | Self::MessageLike { value: MessageLikeEventType::Encrypted } |
| 122 | } |
| 123 | RumaTimelineEventType::RoomMessage => { |
| 124 | Self::MessageLike { value: MessageLikeEventType::RoomMessage } |
| 125 | } |
| 126 | RumaTimelineEventType::CallAnswer => { |
| 127 | Self::MessageLike { value: MessageLikeEventType::CallAnswer } |
| 128 | } |
| 129 | RumaTimelineEventType::CallInvite => { |
| 130 | Self::MessageLike { value: MessageLikeEventType::CallInvite } |
| 131 | } |
| 132 | RumaTimelineEventType::CallHangup => { |
| 133 | Self::MessageLike { value: MessageLikeEventType::CallHangup } |
| 134 | } |
| 135 | RumaTimelineEventType::CallCandidates => { |
| 136 | Self::MessageLike { value: MessageLikeEventType::CallCandidates } |
| 137 | } |
| 138 | RumaTimelineEventType::CallNegotiate => { |
| 139 | Self::MessageLike { value: MessageLikeEventType::CallNegotiate } |
| 140 | } |
| 141 | RumaTimelineEventType::CallReject => { |
| 142 | Self::MessageLike { value: MessageLikeEventType::CallReject } |
| 143 | } |
| 144 | RumaTimelineEventType::CallSdpStreamMetadataChanged => { |
| 145 | Self::MessageLike { value: MessageLikeEventType::CallSdpStreamMetadataChanged } |
| 146 | } |
| 147 | RumaTimelineEventType::CallSelectAnswer => { |
| 148 | Self::MessageLike { value: MessageLikeEventType::CallSelectAnswer } |
| 149 | } |
| 150 | RumaTimelineEventType::KeyVerificationReady => { |
| 151 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationReady } |
| 152 | } |
| 153 | RumaTimelineEventType::KeyVerificationStart => { |
| 154 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationStart } |
| 155 | } |
| 156 | RumaTimelineEventType::KeyVerificationCancel => { |
| 157 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationCancel } |
| 158 | } |
| 159 | RumaTimelineEventType::KeyVerificationAccept => { |
| 160 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationAccept } |
| 161 | } |
| 162 | RumaTimelineEventType::KeyVerificationKey => { |
| 163 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationKey } |
| 164 | } |
| 165 | RumaTimelineEventType::KeyVerificationMac => { |
| 166 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationMac } |
| 167 | } |
| 168 | RumaTimelineEventType::KeyVerificationDone => { |
| 169 | Self::MessageLike { value: MessageLikeEventType::KeyVerificationDone } |
| 170 | } |
| 171 | RumaTimelineEventType::Location => { |
| 172 | Self::MessageLike { value: MessageLikeEventType::Location } |
| 173 | } |
| 174 | RumaTimelineEventType::Message => { |
| 175 | Self::MessageLike { value: MessageLikeEventType::Message } |
| 176 | } |
| 177 | RumaTimelineEventType::PollStart => { |
| 178 | Self::MessageLike { value: MessageLikeEventType::PollStart } |
| 179 | } |
| 180 | RumaTimelineEventType::UnstablePollStart => { |
| 181 | Self::MessageLike { value: MessageLikeEventType::UnstablePollStart } |
| 182 | } |
| 183 | RumaTimelineEventType::PollResponse => { |
| 184 | Self::MessageLike { value: MessageLikeEventType::PollResponse } |
| 185 | } |
| 186 | RumaTimelineEventType::UnstablePollResponse => { |
| 187 | Self::MessageLike { value: MessageLikeEventType::UnstablePollResponse } |
| 188 | } |
| 189 | RumaTimelineEventType::PollEnd => { |
| 190 | Self::MessageLike { value: MessageLikeEventType::PollEnd } |
| 191 | } |
| 192 | RumaTimelineEventType::UnstablePollEnd => { |
| 193 | Self::MessageLike { value: MessageLikeEventType::UnstablePollEnd } |
| 194 | } |
| 195 | RumaTimelineEventType::Beacon => { |
| 196 | Self::MessageLike { value: MessageLikeEventType::Beacon } |
| 197 | } |
| 198 | RumaTimelineEventType::Reaction => { |
| 199 | Self::MessageLike { value: MessageLikeEventType::Reaction } |
| 200 | } |
| 201 | RumaTimelineEventType::RoomEncrypted => { |
| 202 | Self::MessageLike { value: MessageLikeEventType::RoomEncrypted } |
| 203 | } |
| 204 | RumaTimelineEventType::RoomRedaction => { |
| 205 | Self::MessageLike { value: MessageLikeEventType::RoomRedaction } |
| 206 | } |
| 207 | RumaTimelineEventType::Sticker => { |
| 208 | Self::MessageLike { value: MessageLikeEventType::Sticker } |
| 209 | } |
| 210 | RumaTimelineEventType::CallNotify => { |
| 211 | Self::MessageLike { value: MessageLikeEventType::CallNotify } |
| 212 | } |
| 213 | RumaTimelineEventType::RtcNotification => { |
| 214 | Self::MessageLike { value: MessageLikeEventType::RtcNotification } |
| 215 | } |
| 216 | RumaTimelineEventType::RtcDecline => { |
| 217 | Self::MessageLike { value: MessageLikeEventType::RtcDecline } |
| 218 | } |
| 219 | RumaTimelineEventType::PolicyRuleRoom => { |
| 220 | Self::State { value: StateEventType::PolicyRuleRoom } |
| 221 | } |
| 222 | RumaTimelineEventType::PolicyRuleServer => { |
| 223 | Self::State { value: StateEventType::PolicyRuleServer } |
| 224 | } |
| 225 | RumaTimelineEventType::PolicyRuleUser => { |
| 226 | Self::State { value: StateEventType::PolicyRuleUser } |
| 227 | } |
| 228 | RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar }, |
| 229 | RumaTimelineEventType::RoomCanonicalAlias => { |
| 230 | Self::State { value: StateEventType::RoomCanonicalAlias } |
| 231 | } |
| 232 | RumaTimelineEventType::RoomCreate => Self::State { value: StateEventType::RoomCreate }, |
| 233 | RumaTimelineEventType::RoomEncryption => { |
| 234 | Self::State { value: StateEventType::RoomEncryption } |
| 235 | } |
| 236 | RumaTimelineEventType::RoomGuestAccess => { |
| 237 | Self::State { value: StateEventType::RoomGuestAccess } |
| 238 | } |
| 239 | RumaTimelineEventType::RoomHistoryVisibility => { |
| 240 | Self::State { value: StateEventType::RoomHistoryVisibility } |
| 241 | } |
| 242 | RumaTimelineEventType::RoomJoinRules => { |
| 243 | Self::State { value: StateEventType::RoomJoinRules } |
| 244 | } |
| 245 | RumaTimelineEventType::RoomMember => { |
| 246 | Self::State { value: StateEventType::RoomMemberEvent } |
| 247 | } |
| 248 | RumaTimelineEventType::RoomLanguage => { |
| 249 | Self::State { value: StateEventType::RoomLanguage } |
| 250 | } |
| 251 | RumaTimelineEventType::RoomName => Self::State { value: StateEventType::RoomName }, |
| 252 | RumaTimelineEventType::RoomImagePack => { |
| 253 | Self::State { value: StateEventType::RoomImagePack } |
| 254 | } |
| 255 | RumaTimelineEventType::RoomPinnedEvents => { |
| 256 | Self::State { value: StateEventType::RoomPinnedEvents } |
| 257 | } |
| 258 | RumaTimelineEventType::RoomPowerLevels => { |
| 259 | Self::State { value: StateEventType::RoomPowerLevels } |
| 260 | } |
| 261 | RumaTimelineEventType::RoomServerAcl => { |
| 262 | Self::State { value: StateEventType::RoomServerAcl } |
| 263 | } |
| 264 | RumaTimelineEventType::RoomThirdPartyInvite => { |
| 265 | Self::State { value: StateEventType::RoomThirdPartyInvite } |
| 266 | } |
| 267 | RumaTimelineEventType::RoomTombstone => { |
| 268 | Self::State { value: StateEventType::RoomTombstone } |
| 269 | } |
| 270 | RumaTimelineEventType::RoomTopic => Self::State { value: StateEventType::RoomTopic }, |
| 271 | RumaTimelineEventType::SpaceChild => Self::State { value: StateEventType::SpaceChild }, |
| 272 | RumaTimelineEventType::SpaceParent => { |
| 273 | Self::State { value: StateEventType::SpaceParent } |
| 274 | } |
| 275 | RumaTimelineEventType::BeaconInfo => Self::State { value: StateEventType::BeaconInfo }, |
| 276 | RumaTimelineEventType::CallMember => Self::State { value: StateEventType::CallMember }, |
| 277 | RumaTimelineEventType::MemberHints => { |
| 278 | Self::State { value: StateEventType::MemberHints } |
| 279 | } |
| 280 | RumaTimelineEventType::_Custom(_) => { |
| 281 | Self::State { value: StateEventType::Custom { value: value.to_string() } } |
| 282 | } |
| 283 | _ => Self::MessageLike { value: MessageLikeEventType::Other(value.to_string()) }, |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | #[derive(uniffi::Enum)] |
| 289 | // A note about this `allow(clippy::large_enum_variant)`. |
| 290 | // In order to reduce the size of `TimelineEventContent`, we would need to |
| 291 | // put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with |
| 292 | // UniFFI. We would need to change the `uniffi::Record` of the subtypes into |
| 293 | // `uniffi::Object`, which is a radical change. It would simplify the memory |
| 294 | // usage, but it would slow down the performance around the FFI border. Thus, |
| 295 | // let's consider this is a false-positive lint in this particular case. |
| 296 | #[allow(clippy::large_enum_variant)] |
| 297 | pub enum TimelineEventContent { |
| 298 | MessageLike { content: MessageLikeEventContent }, |
| 299 | State { content: StateEventContent }, |
| 300 | } |
| 301 | |
| 302 | #[derive(uniffi::Enum)] |
| 303 | pub enum StateEventContent { |
| 304 | PolicyRuleRoom, |
| 305 | PolicyRuleServer, |
| 306 | PolicyRuleUser, |
| 307 | RoomAvatar, |
| 308 | RoomCanonicalAlias, |
| 309 | RoomCreate, |
| 310 | RoomEncryption, |
| 311 | RoomGuestAccess, |
| 312 | RoomHistoryVisibility, |
| 313 | RoomJoinRules, |
| 314 | RoomMemberContent { user_id: String, membership_state: MembershipState }, |
| 315 | RoomName, |
| 316 | RoomPinnedEvents, |
| 317 | RoomPowerLevels, |
| 318 | RoomServerAcl, |
| 319 | RoomThirdPartyInvite, |
| 320 | RoomTombstone, |
| 321 | RoomTopic { topic: String }, |
| 322 | SpaceChild, |
| 323 | SpaceParent, |
| 324 | } |
| 325 | |
| 326 | impl TryFrom<AnySyncStateEvent> for StateEventContent { |
| 327 | type Error = anyhow::Error; |
| 328 | |
| 329 | fn try_from(value: AnySyncStateEvent) -> anyhow::Result<Self> { |
| 330 | let event = match value { |
| 331 | AnySyncStateEvent::PolicyRuleRoom(_) => StateEventContent::PolicyRuleRoom, |
| 332 | AnySyncStateEvent::PolicyRuleServer(_) => StateEventContent::PolicyRuleServer, |
| 333 | AnySyncStateEvent::PolicyRuleUser(_) => StateEventContent::PolicyRuleUser, |
| 334 | AnySyncStateEvent::RoomAvatar(_) => StateEventContent::RoomAvatar, |
| 335 | AnySyncStateEvent::RoomCanonicalAlias(_) => StateEventContent::RoomCanonicalAlias, |
| 336 | AnySyncStateEvent::RoomCreate(_) => StateEventContent::RoomCreate, |
| 337 | AnySyncStateEvent::RoomEncryption(_) => StateEventContent::RoomEncryption, |
| 338 | AnySyncStateEvent::RoomGuestAccess(_) => StateEventContent::RoomGuestAccess, |
| 339 | AnySyncStateEvent::RoomHistoryVisibility(_) => StateEventContent::RoomHistoryVisibility, |
| 340 | AnySyncStateEvent::RoomJoinRules(_) => StateEventContent::RoomJoinRules, |
| 341 | AnySyncStateEvent::RoomMember(content) => { |
| 342 | let state_key = content.state_key().to_string(); |
| 343 | let original_content = get_state_event_original_content(content)?; |
| 344 | StateEventContent::RoomMemberContent { |
| 345 | user_id: state_key, |
| 346 | membership_state: original_content.membership.try_into()?, |
| 347 | } |
| 348 | } |
| 349 | AnySyncStateEvent::RoomName(_) => StateEventContent::RoomName, |
| 350 | AnySyncStateEvent::RoomPinnedEvents(_) => StateEventContent::RoomPinnedEvents, |
| 351 | AnySyncStateEvent::RoomPowerLevels(_) => StateEventContent::RoomPowerLevels, |
| 352 | AnySyncStateEvent::RoomServerAcl(_) => StateEventContent::RoomServerAcl, |
| 353 | AnySyncStateEvent::RoomThirdPartyInvite(_) => StateEventContent::RoomThirdPartyInvite, |
| 354 | AnySyncStateEvent::RoomTombstone(_) => StateEventContent::RoomTombstone, |
| 355 | AnySyncStateEvent::RoomTopic(content) => { |
| 356 | let content = get_state_event_original_content(content)?; |
| 357 | |
| 358 | StateEventContent::RoomTopic { topic: content.topic } |
| 359 | } |
| 360 | AnySyncStateEvent::SpaceChild(_) => StateEventContent::SpaceChild, |
| 361 | AnySyncStateEvent::SpaceParent(_) => StateEventContent::SpaceParent, |
| 362 | _ => bail!("Unsupported state event: {:?}", value.event_type()), |
| 363 | }; |
| 364 | Ok(event) |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | #[derive(uniffi::Enum)] |
| 369 | // A note about this `allow(clippy::large_enum_variant)`. |
| 370 | // In order to reduce the size of `MessageLineEventContent`, we would need to |
| 371 | // put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with |
| 372 | // UniFFI. We would need to change the `uniffi::Record` of the subtypes into |
| 373 | // `uniffi::Object`, which is a radical change. It would simplify the memory |
| 374 | // usage, but it would slow down the performance around the FFI border. Thus, |
| 375 | // let's consider this is a false-positive lint in this particular case. |
| 376 | #[allow(clippy::large_enum_variant)] |
| 377 | pub enum MessageLikeEventContent { |
| 378 | CallAnswer, |
| 379 | CallInvite, |
| 380 | RtcNotification { |
| 381 | notification_type: RtcNotificationType, |
| 382 | /// The timestamp at which this notification is considered invalid. |
| 383 | expiration_ts: Timestamp, |
| 384 | /// Soft indication of whether it is an audio or video call. |
| 385 | call_intent: Option<RtcCallIntent>, |
| 386 | }, |
| 387 | CallHangup, |
| 388 | CallCandidates, |
| 389 | KeyVerificationReady, |
| 390 | KeyVerificationStart, |
| 391 | KeyVerificationCancel, |
| 392 | KeyVerificationAccept, |
| 393 | KeyVerificationKey, |
| 394 | KeyVerificationMac, |
| 395 | KeyVerificationDone, |
| 396 | Poll { |
| 397 | question: String, |
| 398 | }, |
| 399 | ReactionContent { |
| 400 | related_event_id: String, |
| 401 | }, |
| 402 | RoomEncrypted, |
| 403 | RoomMessage { |
| 404 | message_type: MessageType, |
| 405 | in_reply_to_event_id: Option<String>, |
| 406 | }, |
| 407 | RoomRedaction { |
| 408 | redacted_event_id: Option<String>, |
| 409 | reason: Option<String>, |
| 410 | }, |
| 411 | Sticker, |
| 412 | } |
| 413 | |
| 414 | impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent { |
| 415 | type Error = anyhow::Error; |
| 416 | |
| 417 | fn try_from(value: AnySyncMessageLikeEvent) -> anyhow::Result<Self> { |
| 418 | let content = match value { |
| 419 | AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer, |
| 420 | AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite, |
| 421 | AnySyncMessageLikeEvent::RtcNotification(event) => { |
| 422 | let origin_server_ts = event.origin_server_ts(); |
| 423 | let original_content = get_message_like_event_original_content(event)?; |
| 424 | let expiration_ts = original_content.expiration_ts(origin_server_ts, None).into(); |
| 425 | MessageLikeEventContent::RtcNotification { |
| 426 | notification_type: original_content.notification_type.into(), |
| 427 | expiration_ts, |
| 428 | call_intent: original_content.call_intent.map(|intent| intent.into()), |
| 429 | } |
| 430 | } |
| 431 | AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup, |
| 432 | AnySyncMessageLikeEvent::CallCandidates(_) => MessageLikeEventContent::CallCandidates, |
| 433 | AnySyncMessageLikeEvent::KeyVerificationReady(_) => { |
| 434 | MessageLikeEventContent::KeyVerificationReady |
| 435 | } |
| 436 | AnySyncMessageLikeEvent::KeyVerificationStart(_) => { |
| 437 | MessageLikeEventContent::KeyVerificationStart |
| 438 | } |
| 439 | AnySyncMessageLikeEvent::KeyVerificationCancel(_) => { |
| 440 | MessageLikeEventContent::KeyVerificationCancel |
| 441 | } |
| 442 | AnySyncMessageLikeEvent::KeyVerificationAccept(_) => { |
| 443 | MessageLikeEventContent::KeyVerificationAccept |
| 444 | } |
| 445 | AnySyncMessageLikeEvent::KeyVerificationKey(_) => { |
| 446 | MessageLikeEventContent::KeyVerificationKey |
| 447 | } |
| 448 | AnySyncMessageLikeEvent::KeyVerificationMac(_) => { |
| 449 | MessageLikeEventContent::KeyVerificationMac |
| 450 | } |
| 451 | AnySyncMessageLikeEvent::KeyVerificationDone(_) => { |
| 452 | MessageLikeEventContent::KeyVerificationDone |
| 453 | } |
| 454 | AnySyncMessageLikeEvent::UnstablePollStart(content) => { |
| 455 | let original_content = get_message_like_event_original_content(content)?; |
| 456 | MessageLikeEventContent::Poll { |
| 457 | question: original_content.poll_start().question.text.clone(), |
| 458 | } |
| 459 | } |
| 460 | AnySyncMessageLikeEvent::Reaction(content) => { |
| 461 | let original_content = get_message_like_event_original_content(content)?; |
| 462 | MessageLikeEventContent::ReactionContent { |
| 463 | related_event_id: original_content.relates_to.event_id.to_string(), |
| 464 | } |
| 465 | } |
| 466 | AnySyncMessageLikeEvent::RoomEncrypted(_) => MessageLikeEventContent::RoomEncrypted, |
| 467 | AnySyncMessageLikeEvent::RoomMessage(content) => { |
| 468 | let original_content = get_message_like_event_original_content(content)?; |
| 469 | let in_reply_to_event_id = |
| 470 | original_content.relates_to.and_then(|relation| match relation { |
| 471 | Relation::Reply(reply) => Some(reply.in_reply_to.event_id.to_string()), |
| 472 | _ => None, |
| 473 | }); |
| 474 | MessageLikeEventContent::RoomMessage { |
| 475 | message_type: original_content.msgtype.try_into()?, |
| 476 | in_reply_to_event_id, |
| 477 | } |
| 478 | } |
| 479 | AnySyncMessageLikeEvent::RoomRedaction(c) => { |
| 480 | let (redacted_event_id, reason) = match c { |
| 481 | SyncRoomRedactionEvent::Original(o) => { |
| 482 | let id = |
| 483 | if o.content.redacts.is_some() { o.content.redacts } else { o.redacts }; |
| 484 | (id.map(|id| id.to_string()), o.content.reason) |
| 485 | } |
| 486 | SyncRoomRedactionEvent::Redacted(_) => (None, None), |
| 487 | }; |
| 488 | MessageLikeEventContent::RoomRedaction { redacted_event_id, reason } |
| 489 | } |
| 490 | AnySyncMessageLikeEvent::Sticker(_) => MessageLikeEventContent::Sticker, |
| 491 | _ => bail!("Unsupported Event Type: {:?}", value.event_type()), |
| 492 | }; |
| 493 | Ok(content) |
| 494 | } |
| 495 | } |
| 496 | |
| 497 | fn get_state_event_original_content<C>(event: SyncStateEvent<C>) -> anyhow::Result<C> |
| 498 | where |
| 499 | C: StaticStateEventContent + RedactContent + Clone, |
| 500 | <C as RedactContent>::Redacted: RedactedStateEventContent<StateKey = C::StateKey>, |
| 501 | { |
| 502 | let original_content = |
| 503 | event.as_original().context("Failed to get original content")?.content.clone(); |
| 504 | Ok(original_content) |
| 505 | } |
| 506 | |
| 507 | fn get_message_like_event_original_content<C>(event: SyncMessageLikeEvent<C>) -> anyhow::Result<C> |
| 508 | where |
| 509 | C: RumaMessageLikeEventContent + RedactContent + Clone, |
| 510 | <C as ruma::events::RedactContent>::Redacted: ruma::events::RedactedMessageLikeEventContent, |
| 511 | { |
| 512 | let original_content = |
| 513 | event.as_original().context("Failed to get original content")?.content.clone(); |
| 514 | Ok(original_content) |
| 515 | } |
| 516 | |
| 517 | #[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)] |
| 518 | pub enum StateEventType { |
| 519 | BeaconInfo, |
| 520 | CallMember, |
| 521 | MemberHints, |
| 522 | PolicyRuleRoom, |
| 523 | PolicyRuleServer, |
| 524 | PolicyRuleUser, |
| 525 | RoomAvatar, |
| 526 | RoomCanonicalAlias, |
| 527 | RoomCreate, |
| 528 | RoomEncryption, |
| 529 | RoomGuestAccess, |
| 530 | RoomHistoryVisibility, |
| 531 | RoomImagePack, |
| 532 | RoomJoinRules, |
| 533 | RoomMemberEvent, |
| 534 | RoomLanguage, |
| 535 | RoomName, |
| 536 | RoomPinnedEvents, |
| 537 | RoomPowerLevels, |
| 538 | RoomServerAcl, |
| 539 | RoomThirdPartyInvite, |
| 540 | RoomTombstone, |
| 541 | RoomTopic, |
| 542 | SpaceChild, |
| 543 | SpaceParent, |
| 544 | Custom { value: String }, |
| 545 | } |
| 546 | |
| 547 | impl From<StateEventType> for ruma::events::StateEventType { |
| 548 | fn from(val: StateEventType) -> Self { |
| 549 | match val { |
| 550 | StateEventType::BeaconInfo => Self::BeaconInfo, |
| 551 | StateEventType::CallMember => Self::CallMember, |
| 552 | StateEventType::MemberHints => Self::MemberHints, |
| 553 | StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom, |
| 554 | StateEventType::PolicyRuleServer => Self::PolicyRuleServer, |
| 555 | StateEventType::PolicyRuleUser => Self::PolicyRuleUser, |
| 556 | StateEventType::RoomAvatar => Self::RoomAvatar, |
| 557 | StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias, |
| 558 | StateEventType::RoomCreate => Self::RoomCreate, |
| 559 | StateEventType::RoomEncryption => Self::RoomEncryption, |
| 560 | StateEventType::RoomGuestAccess => Self::RoomGuestAccess, |
| 561 | StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility, |
| 562 | StateEventType::RoomImagePack => Self::RoomImagePack, |
| 563 | StateEventType::RoomJoinRules => Self::RoomJoinRules, |
| 564 | StateEventType::RoomLanguage => Self::RoomLanguage, |
| 565 | StateEventType::RoomMemberEvent => Self::RoomMember, |
| 566 | StateEventType::RoomName => Self::RoomName, |
| 567 | StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents, |
| 568 | StateEventType::RoomPowerLevels => Self::RoomPowerLevels, |
| 569 | StateEventType::RoomServerAcl => Self::RoomServerAcl, |
| 570 | StateEventType::RoomThirdPartyInvite => Self::RoomThirdPartyInvite, |
| 571 | StateEventType::RoomTombstone => Self::RoomTombstone, |
| 572 | StateEventType::RoomTopic => Self::RoomTopic, |
| 573 | StateEventType::SpaceChild => Self::SpaceChild, |
| 574 | StateEventType::SpaceParent => Self::SpaceParent, |
| 575 | StateEventType::Custom { value } => value.into(), |
| 576 | } |
| 577 | } |
| 578 | } |
| 579 | |
| 580 | #[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)] |
| 581 | pub enum MessageLikeEventType { |
| 582 | Audio, |
| 583 | Beacon, |
| 584 | CallAnswer, |
| 585 | CallCandidates, |
| 586 | CallHangup, |
| 587 | CallInvite, |
| 588 | CallNegotiate, |
| 589 | CallNotify, |
| 590 | CallReject, |
| 591 | CallSdpStreamMetadataChanged, |
| 592 | CallSelectAnswer, |
| 593 | Emote, |
| 594 | Encrypted, |
| 595 | File, |
| 596 | Image, |
| 597 | KeyVerificationAccept, |
| 598 | KeyVerificationCancel, |
| 599 | KeyVerificationDone, |
| 600 | KeyVerificationKey, |
| 601 | KeyVerificationMac, |
| 602 | KeyVerificationReady, |
| 603 | KeyVerificationStart, |
| 604 | Location, |
| 605 | Message, |
| 606 | PollEnd, |
| 607 | PollResponse, |
| 608 | PollStart, |
| 609 | Reaction, |
| 610 | RoomEncrypted, |
| 611 | RoomMessage, |
| 612 | RoomRedaction, |
| 613 | RtcDecline, |
| 614 | RtcNotification, |
| 615 | Sticker, |
| 616 | UnstablePollEnd, |
| 617 | UnstablePollResponse, |
| 618 | UnstablePollStart, |
| 619 | Video, |
| 620 | Voice, |
| 621 | Other(String), |
| 622 | } |
| 623 | |
| 624 | impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType { |
| 625 | fn from(val: MessageLikeEventType) -> Self { |
| 626 | match val { |
| 627 | MessageLikeEventType::Audio => Self::Audio, |
| 628 | MessageLikeEventType::File => Self::File, |
| 629 | MessageLikeEventType::Image => Self::Image, |
| 630 | MessageLikeEventType::Video => Self::Video, |
| 631 | MessageLikeEventType::Voice => Self::Voice, |
| 632 | MessageLikeEventType::Beacon => Self::Beacon, |
| 633 | MessageLikeEventType::CallAnswer => Self::CallAnswer, |
| 634 | MessageLikeEventType::CallCandidates => Self::CallCandidates, |
| 635 | MessageLikeEventType::CallInvite => Self::CallInvite, |
| 636 | MessageLikeEventType::CallHangup => Self::CallHangup, |
| 637 | MessageLikeEventType::CallNegotiate => Self::CallNegotiate, |
| 638 | MessageLikeEventType::CallNotify => Self::CallNotify, |
| 639 | MessageLikeEventType::CallReject => Self::CallReject, |
| 640 | MessageLikeEventType::CallSdpStreamMetadataChanged => { |
| 641 | Self::CallSdpStreamMetadataChanged |
| 642 | } |
| 643 | MessageLikeEventType::CallSelectAnswer => Self::CallSelectAnswer, |
| 644 | MessageLikeEventType::Emote => Self::Emote, |
| 645 | MessageLikeEventType::Encrypted => Self::Encrypted, |
| 646 | MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady, |
| 647 | MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart, |
| 648 | MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel, |
| 649 | MessageLikeEventType::KeyVerificationAccept => Self::KeyVerificationAccept, |
| 650 | MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey, |
| 651 | MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac, |
| 652 | MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone, |
| 653 | MessageLikeEventType::Location => Self::Location, |
| 654 | MessageLikeEventType::Message => Self::Message, |
| 655 | MessageLikeEventType::Reaction => Self::Reaction, |
| 656 | MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted, |
| 657 | MessageLikeEventType::RoomMessage => Self::RoomMessage, |
| 658 | MessageLikeEventType::RoomRedaction => Self::RoomRedaction, |
| 659 | MessageLikeEventType::RtcDecline => Self::RtcDecline, |
| 660 | MessageLikeEventType::Sticker => Self::Sticker, |
| 661 | MessageLikeEventType::PollEnd => Self::PollEnd, |
| 662 | MessageLikeEventType::PollResponse => Self::PollResponse, |
| 663 | MessageLikeEventType::PollStart => Self::PollStart, |
| 664 | MessageLikeEventType::RtcNotification => Self::RtcNotification, |
| 665 | MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd, |
| 666 | MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse, |
| 667 | MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart, |
| 668 | MessageLikeEventType::Other(msgtype) => Self::from(msgtype), |
| 669 | } |
| 670 | } |
| 671 | } |
| 672 | |
| 673 | #[derive(Debug, PartialEq, Clone, uniffi::Enum)] |
| 674 | pub enum RoomMessageEventMessageType { |
| 675 | Audio, |
| 676 | Emote, |
| 677 | File, |
| 678 | #[cfg(feature = "unstable-msc4274")] |
| 679 | Gallery, |
| 680 | Image, |
| 681 | Location, |
| 682 | Notice, |
| 683 | ServerNotice, |
| 684 | Text, |
| 685 | Video, |
| 686 | VerificationRequest, |
| 687 | Other, |
| 688 | } |
| 689 | |
| 690 | impl From<RumaMessageType> for RoomMessageEventMessageType { |
| 691 | fn from(val: ruma::events::room::message::MessageType) -> Self { |
| 692 | match val { |
| 693 | RumaMessageType::Audio { .. } => Self::Audio, |
| 694 | RumaMessageType::Emote { .. } => Self::Emote, |
| 695 | RumaMessageType::File { .. } => Self::File, |
| 696 | #[cfg(feature = "unstable-msc4274")] |
| 697 | RumaMessageType::Gallery { .. } => Self::Gallery, |
| 698 | RumaMessageType::Image { .. } => Self::Image, |
| 699 | RumaMessageType::Location { .. } => Self::Location, |
| 700 | RumaMessageType::Notice { .. } => Self::Notice, |
| 701 | RumaMessageType::ServerNotice { .. } => Self::ServerNotice, |
| 702 | RumaMessageType::Text { .. } => Self::Text, |
| 703 | RumaMessageType::Video { .. } => Self::Video, |
| 704 | RumaMessageType::VerificationRequest { .. } => Self::VerificationRequest, |
| 705 | _ => Self::Other, |
| 706 | } |
| 707 | } |
| 708 | } |
| 709 | |
| 710 | /// Contains the 2 possible identifiers of an event, either it has a remote |
| 711 | /// event id or a local transaction id, never both or none. |
| 712 | #[derive(Clone, uniffi::Enum)] |
| 713 | pub enum EventOrTransactionId { |
| 714 | EventId { event_id: String }, |
| 715 | TransactionId { transaction_id: String }, |
| 716 | } |
| 717 | |
| 718 | impl From<TimelineEventItemId> for EventOrTransactionId { |
| 719 | fn from(value: TimelineEventItemId) -> Self { |
| 720 | match value { |
| 721 | TimelineEventItemId::EventId(event_id) => { |
| 722 | EventOrTransactionId::EventId { event_id: event_id.to_string() } |
| 723 | } |
| 724 | TimelineEventItemId::TransactionId(transaction_id) => { |
| 725 | EventOrTransactionId::TransactionId { transaction_id: transaction_id.to_string() } |
| 726 | } |
| 727 | } |
| 728 | } |
| 729 | } |
| 730 | |
| 731 | impl TryFrom<EventOrTransactionId> for TimelineEventItemId { |
| 732 | type Error = IdParseError; |
| 733 | fn try_from(value: EventOrTransactionId) -> Result<Self, Self::Error> { |
| 734 | match value { |
| 735 | EventOrTransactionId::EventId { event_id } => { |
| 736 | Ok(TimelineEventItemId::EventId(EventId::parse(event_id)?)) |
| 737 | } |
| 738 | EventOrTransactionId::TransactionId { transaction_id } => { |
| 739 | Ok(TimelineEventItemId::TransactionId(transaction_id.into())) |
| 740 | } |
| 741 | } |
| 742 | } |
| 743 | } |