use bitflags::bitflags;
use std::sync::Arc;
use matrix_sdk::ruma::{OwnedEventId, UInt, event_id, events::room::message::MessageType};
use matrix_sdk_ui::timeline::{
EventTimelineItem, MsgLikeKind, TimelineEventItemId, TimelineItem, TimelineItemContent,
TimelineItemKind, VirtualTimelineItem,
};
use serde::{Serialize, Serializer};
use crate::{
events::timeline::TimelineKind,
room::frontend_events::{
msg_like::{FrontendStickerEventContent, SerializableReactions},
state_event::{
FrontendAnyOtherStateEventContentChange, FrontendMemberProfileChange,
FrontendRoomMembershipChange, FrontendStateEvent,
},
thread_summary::get_frontend_thread_summary,
timeline_item_id::FrontendTimelineEventItemId,
},
user::user_power_level::UserPowerLevels,
utils::get_or_fetch_event_sender,
};
use super::{
msg_like::{FrontendMsgLikeContent, FrontendMsgLikeKind},
virtual_event::FrontendVirtualTimelineItem,
};
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendTimelineItem {
unique_id: String,
event_id: Option<OwnedEventId>,
#[serde(flatten)]
timeline_item_id: FrontendTimelineEventItemId,
#[serde(flatten)]
data: FrontendTimelineItemData,
timestamp: Option<UInt>, is_own: bool,
is_local: bool,
abilities: MessageAbilities,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Serialize)]
#[serde(
rename_all = "camelCase",
rename_all_fields = "camelCase",
tag = "kind",
content = "data"
)]
pub enum FrontendTimelineItemData {
MsgLike(FrontendMsgLikeContent),
Virtual(FrontendVirtualTimelineItem),
StateChange(FrontendStateEvent),
Error(FrontendTimelineErrorItem),
Call,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendTimelineErrorItem {
error: String,
}
pub fn to_frontend_timeline_item(
item: &Arc<TimelineItem>,
timeline_kind: &TimelineKind,
user_power_levels: &UserPowerLevels,
) -> Option<FrontendTimelineItem> {
let unique_id = item.unique_id().0.clone();
match item.kind() {
TimelineItemKind::Event(event_tl_item) => {
map_event_timeline_item(unique_id, event_tl_item, timeline_kind, user_power_levels)
}
TimelineItemKind::Virtual(event) => match event {
VirtualTimelineItem::DateDivider(timestamp) => Some(FrontendTimelineItem {
unique_id,
event_id: None,
timeline_item_id: TimelineEventItemId::EventId(
event_id!("$no_ids_for_virtual").to_owned(),
)
.into(),
data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::DateDivider),
is_local: true,
is_own: true,
timestamp: Some(timestamp.0),
abilities: MessageAbilities::empty(),
}),
VirtualTimelineItem::ReadMarker => Some(FrontendTimelineItem {
unique_id,
event_id: None,
timeline_item_id: TimelineEventItemId::EventId(
event_id!("$no_ids_for_virtual").to_owned(),
)
.into(),
data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::ReadMarker),
is_local: true,
is_own: true,
timestamp: None,
abilities: MessageAbilities::empty(),
}),
VirtualTimelineItem::TimelineStart => Some(FrontendTimelineItem {
unique_id,
event_id: None,
timeline_item_id: TimelineEventItemId::EventId(
event_id!("$no_ids_for_virtual").to_owned(),
)
.into(),
data: FrontendTimelineItemData::Virtual(FrontendVirtualTimelineItem::TimelineStart),
is_local: true,
is_own: true,
timestamp: None,
abilities: MessageAbilities::empty(),
}),
},
}
}
fn map_msg_event_content(content: MessageType) -> FrontendMsgLikeKind {
match content {
MessageType::Audio(c) => FrontendMsgLikeKind::Audio(c),
MessageType::File(c) => FrontendMsgLikeKind::File(c),
MessageType::Image(c) => FrontendMsgLikeKind::Image(c),
MessageType::Text(c) => FrontendMsgLikeKind::Text(c),
MessageType::Video(c) => FrontendMsgLikeKind::Video(c),
MessageType::Emote(c) => FrontendMsgLikeKind::Emote(c),
MessageType::Location(c) => FrontendMsgLikeKind::Location(c),
MessageType::Notice(c) => FrontendMsgLikeKind::Notice(c),
MessageType::ServerNotice(c) => FrontendMsgLikeKind::ServerNotice(c),
MessageType::VerificationRequest(c) => FrontendMsgLikeKind::VerificationRequest(c),
_ => FrontendMsgLikeKind::Unknown,
}
}
bitflags! {
#[derive(Copy, Clone, Debug)]
pub struct MessageAbilities: u8 {
const CanReact = 1 << 0;
const CanReplyTo = 1 << 1;
const CanEdit = 1 << 2;
const CanPin = 1 << 3;
const CanUnpin = 1 << 4;
const CanDelete = 1 << 5;
}
}
impl MessageAbilities {
pub fn from_user_power_and_event(
user_power_levels: &UserPowerLevels,
event_tl_item: &EventTimelineItem,
) -> Self {
let mut abilities = Self::empty();
abilities.set(Self::CanEdit, event_tl_item.is_editable());
if event_tl_item.is_own() {
abilities.set(Self::CanDelete, user_power_levels._can_redact_own());
}
abilities.set(Self::CanReplyTo, event_tl_item.can_be_replied_to());
abilities.set(Self::CanPin, user_power_levels._can_pin());
abilities.set(Self::CanReact, user_power_levels._can_send_reaction());
abilities
}
}
impl Serialize for MessageAbilities {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(None)?;
if self.contains(MessageAbilities::CanReact) {
seq.serialize_element("canReact")?;
}
if self.contains(MessageAbilities::CanReplyTo) {
seq.serialize_element("canReplyTo")?;
}
if self.contains(MessageAbilities::CanEdit) {
seq.serialize_element("canEdit")?;
}
if self.contains(MessageAbilities::CanPin) {
seq.serialize_element("canPin")?;
}
if self.contains(MessageAbilities::CanUnpin) {
seq.serialize_element("canUnpin")?;
}
if self.contains(MessageAbilities::CanDelete) {
seq.serialize_element("canDelete")?;
}
seq.end()
}
}
pub(crate) fn map_event_timeline_item(
unique_id: String,
event_tl_item: &EventTimelineItem,
kind: &TimelineKind,
user_power_levels: &UserPowerLevels,
) -> Option<FrontendTimelineItem> {
let timeline_item_id: FrontendTimelineEventItemId = event_tl_item.identifier().into();
let is_own = event_tl_item.is_own();
let is_local = event_tl_item.is_local_echo();
let timestamp = Some(event_tl_item.timestamp().get());
let sender = Some(get_or_fetch_event_sender(event_tl_item, Some(kind.clone())));
let sender_id = event_tl_item.sender().to_string();
let abilities = MessageAbilities::from_user_power_and_event(user_power_levels, event_tl_item);
let event_id = event_tl_item.event_id().map(|id| id.to_owned());
map_timeline_event_item_content(
event_tl_item.content(),
unique_id,
timeline_item_id,
is_own,
is_local,
timestamp,
sender,
sender_id,
abilities,
event_id,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn map_timeline_event_item_content(
timeline_item_content: &TimelineItemContent,
unique_id: String,
timeline_item_id: FrontendTimelineEventItemId,
is_own: bool,
is_local: bool,
timestamp: Option<UInt>,
sender: Option<String>,
sender_id: String,
abilities: MessageAbilities,
event_id: Option<OwnedEventId>,
) -> Option<FrontendTimelineItem> {
match timeline_item_content {
TimelineItemContent::MsgLike(msg_like) => {
let in_reply_to_id = msg_like.in_reply_to.clone().map(|r| r.event_id);
let thread_root = msg_like.thread_root.clone();
let thread_summary = msg_like
.thread_summary
.clone()
.and_then(get_frontend_thread_summary);
match msg_like.kind.clone() {
MsgLikeKind::Message(message) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
edited: message.is_edited(),
reactions: SerializableReactions(msg_like.reactions.clone()),
sender_id,
sender,
thread_root,
thread_summary,
in_reply_to_id,
kind: map_msg_event_content(message.msgtype().clone()),
}),
}),
MsgLikeKind::Sticker(sticker) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
edited: false,
reactions: SerializableReactions(msg_like.reactions.clone()),
sender_id,
sender,
thread_root,
thread_summary,
in_reply_to_id,
kind: FrontendMsgLikeKind::Sticker(Box::new(
FrontendStickerEventContent::from(sticker.content().clone()),
)),
}),
}),
MsgLikeKind::Redacted => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
edited: true,
reactions: SerializableReactions(msg_like.reactions.clone()),
sender_id,
sender,
thread_root,
thread_summary,
in_reply_to_id,
kind: FrontendMsgLikeKind::Redacted,
}),
}),
MsgLikeKind::UnableToDecrypt(_) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
edited: false,
reactions: SerializableReactions(msg_like.reactions.clone()),
sender_id,
sender,
thread_root,
thread_summary,
in_reply_to_id,
kind: FrontendMsgLikeKind::UnableToDecrypt,
}),
}),
MsgLikeKind::LiveLocation(_) => None,
MsgLikeKind::Poll(_) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
edited: false,
reactions: SerializableReactions(msg_like.reactions.clone()),
sender_id,
sender,
thread_root,
thread_summary,
in_reply_to_id,
kind: FrontendMsgLikeKind::Poll,
}),
}),
MsgLikeKind::Other(_) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::MsgLike(FrontendMsgLikeContent {
edited: false,
reactions: SerializableReactions(msg_like.reactions.clone()),
sender_id,
sender,
thread_root,
thread_summary,
in_reply_to_id,
kind: FrontendMsgLikeKind::Unknown,
}),
}),
}
}
TimelineItemContent::OtherState(state) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::StateChange(FrontendStateEvent::OtherState(
FrontendAnyOtherStateEventContentChange::from(state.content().clone()),
)),
}),
TimelineItemContent::MembershipChange(change) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::StateChange(FrontendStateEvent::MembershipChange(
FrontendRoomMembershipChange::from(change.clone()),
)),
}),
TimelineItemContent::ProfileChange(change) => Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::StateChange(FrontendStateEvent::ProfileChange(
FrontendMemberProfileChange::from(change.clone()),
)),
}),
TimelineItemContent::RtcNotification { .. } | TimelineItemContent::CallInvite => {
Some(FrontendTimelineItem {
unique_id,
event_id,
timeline_item_id,
is_local,
is_own,
timestamp,
abilities,
data: FrontendTimelineItemData::Call,
})
}
TimelineItemContent::FailedToParseMessageLike {
event_type: _,
error,
} => Some(FrontendTimelineItem {
unique_id,
event_id: None,
timeline_item_id,
data: FrontendTimelineItemData::Error(FrontendTimelineErrorItem {
error: error.to_string(),
}),
is_local: true,
is_own: true,
timestamp: None,
abilities,
}),
TimelineItemContent::FailedToParseState {
state_key: _,
event_type: _,
error,
} => Some(FrontendTimelineItem {
unique_id,
event_id: None,
timeline_item_id,
data: FrontendTimelineItemData::Error(FrontendTimelineErrorItem {
error: error.to_string(),
}),
is_local: true,
is_own: true,
timestamp: None,
abilities,
}),
}
}