use std::{
ops::{Deref, DerefMut},
sync::{Arc, LazyLock},
};
use as_variant::as_variant;
use indexmap::IndexMap;
use matrix_sdk::{
Error, Room,
deserialized_responses::{EncryptionInfo, ShieldState},
send_queue::{SendHandle, SendReactionHandle},
};
use matrix_sdk_base::deserialized_responses::ShieldStateCode;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
OwnedUserId, TransactionId, UserId,
events::{AnySyncTimelineEvent, receipt::Receipt, room::message::MessageType},
room_version_rules::RedactionRules,
serde::Raw,
};
use tracing::error;
use unicode_segmentation::UnicodeSegmentation;
mod content;
mod local;
mod remote;
pub use self::{
content::{
AnyOtherStateEventContentChange, BeaconInfo, EmbeddedEvent, EncryptedMessage,
InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, Message,
MsgLikeContent, MsgLikeKind, OtherMessageLike, OtherState, PollResult, PollState,
RoomMembershipChange, RoomPinnedEventsChange, Sticker, ThreadSummary, TimelineItemContent,
},
local::{EventSendState, MediaUploadProgress},
};
pub(super) use self::{
content::{
beacon_info_matches, extract_bundled_edit_event_json, extract_poll_edit_content,
extract_room_msg_edit_content,
},
local::LocalEventTimelineItem,
remote::{RemoteEventOrigin, RemoteEventTimelineItem},
};
#[derive(Clone, Debug)]
pub struct EventTimelineItem {
pub(super) sender: OwnedUserId,
pub(super) sender_profile: TimelineDetails<Profile>,
pub(super) forwarder: Option<OwnedUserId>,
pub(super) forwarder_profile: Option<TimelineDetails<Profile>>,
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
pub(super) content: TimelineItemContent,
pub(super) unredacted_item: Option<UnredactedEventTimelineItem>,
pub(super) kind: EventTimelineItemKind,
pub(super) is_room_encrypted: bool,
}
#[derive(Clone, Debug)]
pub(super) enum EventTimelineItemKind {
Local(LocalEventTimelineItem),
Remote(RemoteEventTimelineItem),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum TimelineEventItemId {
TransactionId(OwnedTransactionId),
EventId(OwnedEventId),
}
pub(crate) enum TimelineItemHandle<'a> {
Remote(&'a EventId),
Local(&'a SendHandle),
}
#[derive(Clone, Debug)]
pub(super) struct UnredactedEventTimelineItem {
content: TimelineItemContent,
pub(crate) original_json: Option<Raw<AnySyncTimelineEvent>>,
pub(crate) latest_edit_json: Option<Raw<AnySyncTimelineEvent>>,
}
impl EventTimelineItem {
#[allow(clippy::too_many_arguments)]
pub(super) fn new(
sender: OwnedUserId,
sender_profile: TimelineDetails<Profile>,
forwarder: Option<OwnedUserId>,
forwarder_profile: Option<TimelineDetails<Profile>>,
timestamp: MilliSecondsSinceUnixEpoch,
content: TimelineItemContent,
kind: EventTimelineItemKind,
is_room_encrypted: bool,
) -> Self {
Self {
sender,
sender_profile,
forwarder,
forwarder_profile,
timestamp,
content,
unredacted_item: None,
kind,
is_room_encrypted,
}
}
pub fn is_local_echo(&self) -> bool {
matches!(self.kind, EventTimelineItemKind::Local(_))
}
pub fn is_remote_event(&self) -> bool {
matches!(self.kind, EventTimelineItemKind::Remote(_))
}
pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
}
pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
}
pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
}
pub fn send_state(&self) -> Option<&EventSendState> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
}
pub fn local_created_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
match &self.kind {
EventTimelineItemKind::Local(local) => local.send_handle.as_ref().map(|s| s.created_at),
EventTimelineItemKind::Remote(_) => None,
}
}
pub fn identifier(&self) -> TimelineEventItemId {
match &self.kind {
EventTimelineItemKind::Local(local) => local.identifier(),
EventTimelineItemKind::Remote(remote) => {
TimelineEventItemId::EventId(remote.event_id.clone())
}
}
}
pub fn transaction_id(&self) -> Option<&TransactionId> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
}
pub fn event_id(&self) -> Option<&EventId> {
match &self.kind {
EventTimelineItemKind::Local(local_event) => local_event.event_id(),
EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
}
}
pub fn sender(&self) -> &UserId {
&self.sender
}
pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
&self.sender_profile
}
pub fn forwarder(&self) -> Option<&UserId> {
self.forwarder.as_deref()
}
pub fn forwarder_profile(&self) -> Option<&TimelineDetails<Profile>> {
self.forwarder_profile.as_ref()
}
pub fn content(&self) -> &TimelineItemContent {
&self.content
}
pub(crate) fn content_mut(&mut self) -> &mut TimelineItemContent {
&mut self.content
}
pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
static EMPTY_RECEIPTS: LazyLock<IndexMap<OwnedUserId, Receipt>> =
LazyLock::new(Default::default);
match &self.kind {
EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
}
}
pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
self.timestamp
}
pub fn is_own(&self) -> bool {
match &self.kind {
EventTimelineItemKind::Local(_) => true,
EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
}
}
pub fn is_editable(&self) -> bool {
if !self.is_own() {
return false;
}
match self.content() {
TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
MsgLikeKind::Message(message) => match message.msgtype() {
MessageType::Text(_)
| MessageType::Emote(_)
| MessageType::Audio(_)
| MessageType::File(_)
| MessageType::Image(_)
| MessageType::Video(_) => true,
#[cfg(feature = "unstable-msc4274")]
MessageType::Gallery(_) => true,
_ => false,
},
MsgLikeKind::Poll(poll) => {
poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
}
_ => false,
},
_ => {
false
}
}
}
pub fn is_highlighted(&self) -> bool {
match &self.kind {
EventTimelineItemKind::Local(_) => false,
EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
}
}
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_deref(),
}
}
pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
if !self.is_room_encrypted || self.is_local_echo() {
return TimelineEventShieldState::None;
}
if self.content().is_unable_to_decrypt() {
return TimelineEventShieldState::None;
}
if let Some(live_location) = self.content().as_live_location_state() {
return match live_location.latest_location() {
None => TimelineEventShieldState::None,
Some(beacon) => match beacon.encryption_info() {
Some(info) => {
if strict {
info.verification_state.to_shield_state_strict().into()
} else {
info.verification_state.to_shield_state_lax().into()
}
}
None => TimelineEventShieldState::Red {
code: TimelineEventShieldStateCode::SentInClear,
},
},
};
}
match self.encryption_info() {
Some(info) => {
if strict {
info.verification_state.to_shield_state_strict().into()
} else {
info.verification_state.to_shield_state_lax().into()
}
}
None => {
TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
}
}
}
pub fn can_be_replied_to(&self) -> bool {
if self.event_id().is_none() {
false
} else if self.content.is_message() {
true
} else {
self.latest_json().is_some()
}
}
pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
}
}
pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
}
}
pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
self.latest_edit_json().or_else(|| self.original_json())
}
pub fn origin(&self) -> Option<EventItemOrigin> {
match &self.kind {
EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
RemoteEventOrigin::Cache => Some(EventItemOrigin::Cache),
RemoteEventOrigin::Unknown => None,
},
}
}
pub(super) fn set_content(&mut self, content: TimelineItemContent) {
self.content = content;
}
pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
Self { kind: kind.into(), ..self.clone() }
}
pub(super) fn with_content(&self, new_content: TimelineItemContent) -> Self {
let mut new = self.clone();
new.content = new_content;
new
}
pub(super) fn with_content_and_latest_edit(
&self,
new_content: TimelineItemContent,
edit_json: Option<Raw<AnySyncTimelineEvent>>,
) -> Self {
let mut new = self.clone();
new.content = new_content;
if let EventTimelineItemKind::Remote(r) = &mut new.kind {
r.latest_edit_json = edit_json;
}
new
}
pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
Self { sender_profile, ..self.clone() }
}
pub(super) fn with_encryption_info(
&self,
encryption_info: Option<Arc<EncryptionInfo>>,
) -> Self {
let mut new = self.clone();
if let EventTimelineItemKind::Remote(r) = &mut new.kind {
r.encryption_info = encryption_info;
}
new
}
pub(super) fn redact(&self, rules: &RedactionRules, is_local: bool) -> Self {
let unredacted_item = is_local.then(|| UnredactedEventTimelineItem {
content: self.content.clone(),
original_json: self.original_json().cloned(),
latest_edit_json: self.latest_edit_json().cloned(),
});
let content = self.content.redact(rules);
let kind = match &self.kind {
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
};
Self {
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
forwarder: self.forwarder.clone(),
forwarder_profile: self.forwarder_profile.clone(),
timestamp: self.timestamp,
content,
unredacted_item,
kind,
is_room_encrypted: self.is_room_encrypted,
}
}
pub(super) fn unredact(&self) -> Self {
let Some(unredacted_item) = &self.unredacted_item else { return self.clone() };
let kind = match &self.kind {
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
EventTimelineItemKind::Remote(r) => {
EventTimelineItemKind::Remote(RemoteEventTimelineItem {
original_json: unredacted_item.original_json.clone(),
latest_edit_json: unredacted_item.latest_edit_json.clone(),
..r.clone()
})
}
};
Self {
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
forwarder: self.forwarder.clone(),
forwarder_profile: self.forwarder_profile.clone(),
timestamp: self.timestamp,
content: unredacted_item.content.clone(),
unredacted_item: None,
kind,
is_room_encrypted: self.is_room_encrypted,
}
}
pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
match &self.kind {
EventTimelineItemKind::Local(local) => {
if let Some(event_id) = local.event_id() {
TimelineItemHandle::Remote(event_id)
} else {
TimelineItemHandle::Local(
local.send_handle.as_ref().expect("Unexpected missing send_handle"),
)
}
}
EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
}
}
pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
}
pub fn contains_only_emojis(&self) -> bool {
let body = match self.content() {
TimelineItemContent::MsgLike(msglike) => match &msglike.kind {
MsgLikeKind::Message(message) => match &message.msgtype {
MessageType::Text(text) => Some(text.body.as_str()),
MessageType::Audio(audio) => audio.caption(),
MessageType::File(file) => file.caption(),
MessageType::Image(image) => image.caption(),
MessageType::Video(video) => video.caption(),
_ => None,
},
MsgLikeKind::Sticker(_)
| MsgLikeKind::Poll(_)
| MsgLikeKind::Redacted
| MsgLikeKind::UnableToDecrypt(_)
| MsgLikeKind::Other(_)
| MsgLikeKind::LiveLocation(_) => None,
},
TimelineItemContent::MembershipChange(_)
| TimelineItemContent::ProfileChange(_)
| TimelineItemContent::OtherState(_)
| TimelineItemContent::FailedToParseMessageLike { .. }
| TimelineItemContent::FailedToParseState { .. }
| TimelineItemContent::CallInvite
| TimelineItemContent::RtcNotification { .. } => None,
};
if let Some(body) = body {
let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
if graphemes.len() > 5 {
return false;
}
graphemes.iter().all(|g| emojis::get(g).is_some())
} else {
false
}
}
}
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
fn from(value: LocalEventTimelineItem) -> Self {
EventTimelineItemKind::Local(value)
}
}
impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
fn from(value: RemoteEventTimelineItem) -> Self {
EventTimelineItemKind::Remote(value)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Profile {
pub display_name: Option<String>,
pub display_name_ambiguous: bool,
pub avatar_url: Option<OwnedMxcUri>,
}
impl Profile {
pub async fn load(room: &Room, user_id: &UserId) -> Option<Self> {
match room.get_member_no_sync(user_id).await {
Ok(Some(member)) => Some(Profile {
display_name: member.display_name().map(ToOwned::to_owned),
display_name_ambiguous: member.name_ambiguous(),
avatar_url: member.avatar_url().map(ToOwned::to_owned),
}),
Ok(None) if room.are_members_synced() => Some(Profile::default()),
Ok(None) => None,
Err(e) => {
error!(%user_id, "Failed to fetch room member information: {e}");
None
}
}
}
}
#[derive(Clone, Debug)]
pub enum TimelineDetails<T> {
Unavailable,
Pending,
Ready(T),
Error(Arc<Error>),
}
impl<T> TimelineDetails<T> {
pub fn from_initial_value(value: Option<T>) -> Self {
match value {
Some(v) => Self::Ready(v),
None => Self::Unavailable,
}
}
pub fn is_unavailable(&self) -> bool {
matches!(self, Self::Unavailable)
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready(_))
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum EventItemOrigin {
Local,
Sync,
Pagination,
Cache,
}
#[derive(Clone, Debug)]
pub enum ReactionStatus {
LocalToLocal(Option<SendReactionHandle>),
LocalToRemote(Option<SendHandle>),
RemoteToRemote(OwnedEventId),
}
#[derive(Clone, Debug)]
pub struct ReactionInfo {
pub timestamp: MilliSecondsSinceUnixEpoch,
pub status: ReactionStatus,
}
#[derive(Debug, Clone, Default)]
pub struct ReactionsByKeyBySender(IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>);
impl Deref for ReactionsByKeyBySender {
type Target = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ReactionsByKeyBySender {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl ReactionsByKeyBySender {
pub(crate) fn remove_reaction(
&mut self,
sender: &UserId,
annotation: &str,
) -> Option<ReactionInfo> {
if let Some(by_user) = self.0.get_mut(annotation)
&& let Some(info) = by_user.swap_remove(sender)
{
if by_user.is_empty() {
self.0.swap_remove(annotation);
}
return Some(info);
}
None
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimelineEventShieldState {
Red {
code: TimelineEventShieldStateCode,
},
Grey {
code: TimelineEventShieldStateCode,
},
None,
}
impl From<ShieldState> for TimelineEventShieldState {
fn from(value: ShieldState) -> Self {
match value {
ShieldState::Red { code, message: _ } => {
TimelineEventShieldState::Red { code: code.into() }
}
ShieldState::Grey { code, message: _ } => {
TimelineEventShieldState::Grey { code: code.into() }
}
ShieldState::None => TimelineEventShieldState::None,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum TimelineEventShieldStateCode {
AuthenticityNotGuaranteed,
UnknownDevice,
UnsignedDevice,
UnverifiedIdentity,
VerificationViolation,
MismatchedSender,
SentInClear,
}
impl From<ShieldStateCode> for TimelineEventShieldStateCode {
fn from(value: ShieldStateCode) -> Self {
use TimelineEventShieldStateCode::*;
match value {
ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
ShieldStateCode::UnknownDevice => UnknownDevice,
ShieldStateCode::UnsignedDevice => UnsignedDevice,
ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
ShieldStateCode::VerificationViolation => VerificationViolation,
ShieldStateCode::MismatchedSender => MismatchedSender,
}
}
}