fractal/utils/matrix/
ext_traits.rs1use std::borrow::Cow;
4
5use gtk::{glib, prelude::*};
6use matrix_sdk_ui::timeline::{
7 EventTimelineItem, MembershipChange, Message, MsgLikeKind, TimelineEventItemId,
8 TimelineItemContent,
9};
10use ruma::{
11 UserId,
12 events::{
13 AnySyncTimelineEvent,
14 room::message::{FormattedBody, MessageType},
15 },
16 serde::Raw,
17};
18use serde::Deserialize;
19
20use crate::utils::string::StrMutExt;
21
22pub(crate) trait AtMentionExt {
24 fn can_contain_at_room(&self) -> bool;
29}
30
31impl AtMentionExt for TimelineItemContent {
32 fn can_contain_at_room(&self) -> bool {
33 match self {
34 TimelineItemContent::MsgLike(msg_like) => match &msg_like.kind {
35 MsgLikeKind::Message(message) => message.can_contain_at_room(),
36 _ => false,
37 },
38 _ => false,
39 }
40 }
41}
42
43impl AtMentionExt for Message {
44 fn can_contain_at_room(&self) -> bool {
45 self.mentions().is_none_or(|mentions| mentions.room)
46 }
47}
48
49pub(crate) trait TimelineEventItemIdExt: Sized {
51 fn static_variant_type() -> Cow<'static, glib::VariantTy>;
53
54 fn to_variant(&self) -> glib::Variant;
56
57 fn from_variant(variant: &glib::Variant) -> Option<Self>;
59}
60
61impl TimelineEventItemIdExt for TimelineEventItemId {
62 fn static_variant_type() -> Cow<'static, glib::VariantTy> {
63 Cow::Borrowed(glib::VariantTy::STRING)
64 }
65
66 fn to_variant(&self) -> glib::Variant {
67 let s = match self {
68 Self::TransactionId(txn_id) => format!("transaction_id:{txn_id}"),
69 Self::EventId(event_id) => format!("event_id:{event_id}"),
70 };
71
72 s.to_variant()
73 }
74
75 fn from_variant(variant: &glib::Variant) -> Option<Self> {
76 let s = variant.str()?;
77
78 if let Some(s) = s.strip_prefix("transaction_id:") {
79 Some(Self::TransactionId(s.into()))
80 } else if let Some(s) = s.strip_prefix("event_id:") {
81 s.try_into().ok().map(Self::EventId)
82 } else {
83 None
84 }
85 }
86}
87
88pub(crate) trait TimelineItemContentExt {
90 fn counts_as_unread(&self) -> bool;
97
98 fn counts_as_activity(&self, own_user_id: &UserId) -> bool;
104
105 fn can_show_header(&self) -> bool;
107
108 fn is_edited(&self) -> bool;
110}
111
112impl TimelineItemContentExt for TimelineItemContent {
113 fn counts_as_unread(&self) -> bool {
114 match self {
115 TimelineItemContent::MsgLike(msg_like) => match &msg_like.kind {
116 MsgLikeKind::Message(message) => {
117 !matches!(message.msgtype(), MessageType::Notice(_))
118 }
119 MsgLikeKind::Sticker(_) => true,
120 _ => false,
121 },
122 _ => false,
123 }
124 }
125
126 fn counts_as_activity(&self, own_user_id: &UserId) -> bool {
127 if self.counts_as_unread() {
128 return true;
129 }
130
131 match self {
132 TimelineItemContent::MembershipChange(membership) => {
133 if membership.user_id() != own_user_id {
134 return false;
135 }
136
137 membership.change().is_none_or(|change| {
145 matches!(
146 change,
147 MembershipChange::Joined
148 | MembershipChange::Unbanned
149 | MembershipChange::Invited
150 | MembershipChange::InvitationAccepted
151 | MembershipChange::KnockAccepted
152 | MembershipChange::Knocked
153 )
154 })
155 }
156 _ => false,
157 }
158 }
159
160 fn can_show_header(&self) -> bool {
161 match self {
162 TimelineItemContent::MsgLike(msg_like) => match &msg_like.kind {
163 MsgLikeKind::Message(message) => {
164 matches!(
165 message.msgtype(),
166 MessageType::Audio(_)
167 | MessageType::File(_)
168 | MessageType::Image(_)
169 | MessageType::Location(_)
170 | MessageType::Notice(_)
171 | MessageType::Text(_)
172 | MessageType::Video(_)
173 )
174 }
175 MsgLikeKind::Sticker(_) | MsgLikeKind::UnableToDecrypt(_) => true,
176 _ => false,
177 },
178 _ => false,
179 }
180 }
181
182 fn is_edited(&self) -> bool {
183 match self {
184 TimelineItemContent::MsgLike(msg_like) => {
185 matches!(&msg_like.kind, MsgLikeKind::Message(message) if message.is_edited())
186 }
187 _ => false,
188 }
189 }
190}
191
192pub(crate) trait EventTimelineItemExt {
194 fn latest_edit_raw(&self) -> Option<Raw<AnySyncTimelineEvent>>;
196}
197
198impl EventTimelineItemExt for EventTimelineItem {
199 fn latest_edit_raw(&self) -> Option<Raw<AnySyncTimelineEvent>> {
201 if let Some(raw) = self.latest_edit_json() {
202 return Some(raw.clone());
203 }
204
205 self.original_json()?
206 .get_field::<RawUnsigned>("unsigned")
207 .ok()
208 .flatten()?
209 .relations?
210 .replace
211 }
212}
213
214#[derive(Debug, Clone, Deserialize)]
218struct RawUnsigned {
219 #[serde(rename = "m.relations")]
220 relations: Option<RawBundledRelations>,
221}
222
223#[derive(Debug, Clone, Deserialize)]
227struct RawBundledRelations {
228 #[serde(rename = "m.replace")]
229 replace: Option<Raw<AnySyncTimelineEvent>>,
230}
231
232pub(crate) trait FormattedBodyExt {
234 fn clean_string(&mut self);
238}
239
240impl FormattedBodyExt for Option<FormattedBody> {
241 fn clean_string(&mut self) {
242 self.take_if(|formatted| {
243 formatted.body.clean_string();
244 formatted.body.is_empty()
245 });
246 }
247}