tauri_plugin_matrix_svelte/matrix/
utils.rs1use std::borrow::Cow;
2use tokio::sync::{broadcast, mpsc};
3use tokio::time::{sleep, Duration};
4
5use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId, RoomId};
6use matrix_sdk_ui::timeline::{EventTimelineItem, TimelineDetails};
7
8use super::{
9 requests::{submit_async_request, MatrixRequest},
10 singletons::CLIENT,
11};
12
13pub fn get_or_fetch_event_sender(
19 event_tl_item: &EventTimelineItem,
20 room_id: Option<&OwnedRoomId>,
21) -> String {
22 let sender_username = match event_tl_item.sender_profile() {
23 TimelineDetails::Ready(profile) => profile.display_name.as_deref(),
24 TimelineDetails::Unavailable => {
25 if let Some(room_id) = room_id {
26 if let Some(event_id) = event_tl_item.event_id() {
27 submit_async_request(MatrixRequest::FetchDetailsForEvent {
29 room_id: room_id.clone(),
30 event_id: event_id.to_owned(),
31 });
32 }
33 }
34 None
35 }
36 _ => None,
37 }
38 .unwrap_or_else(|| event_tl_item.sender().as_str());
39 sender_username.to_owned()
40}
41
42pub fn current_user_id() -> Option<OwnedUserId> {
44 CLIENT
45 .get()
46 .and_then(|c| c.session_meta().map(|m| m.user_id.clone()))
47}
48
49pub fn trim_start_html_whitespace(mut text: &str) -> &str {
51 let mut prev_text_len = text.len();
52 loop {
53 text = text
54 .trim_start_matches("<p>")
55 .trim_start_matches("<br>")
56 .trim_start_matches("<br/>")
57 .trim_start_matches("<br />")
58 .trim_start();
59
60 if text.len() == prev_text_len {
61 break;
62 }
63 prev_text_len = text.len();
64 }
65 text
66}
67
68pub fn linkify(text: &str, is_html: bool) -> Cow<'_, str> {
70 use linkify::{LinkFinder, LinkKind};
71 let mut links = LinkFinder::new().links(text).peekable();
72 if links.peek().is_none() {
73 return Cow::Borrowed(text);
74 }
75
76 let escaped = |text| {
78 if is_html {
79 Cow::from(text)
80 } else {
81 htmlize::escape_text(text)
82 }
83 };
84
85 let mut linkified_text = String::new();
86 let mut last_end_index = 0;
87 for link in links {
88 let link_txt = link.as_str();
89 let is_link_within_href_attr = text.get(..link.start()).is_some_and(ends_with_href);
91 let is_link_within_html_tag = text
92 .get(link.end()..)
93 .is_some_and(|after| after.trim_end().starts_with("</a>"));
94
95 if is_link_within_href_attr || is_link_within_html_tag {
96 linkified_text = format!(
97 "{linkified_text}{}",
98 text.get(last_end_index..link.end()).unwrap_or_default(),
99 );
100 } else {
101 match link.kind() {
102 LinkKind::Url => {
103 linkified_text = format!(
104 "{linkified_text}{}<a href=\"{}\">{}</a>",
105 escaped(text.get(last_end_index..link.start()).unwrap_or_default()),
106 htmlize::escape_attribute(link_txt),
107 htmlize::escape_text(link_txt),
108 );
109 }
110 LinkKind::Email => {
111 linkified_text = format!(
112 "{linkified_text}{}<a href=\"mailto:{}\">{}</a>",
113 escaped(text.get(last_end_index..link.start()).unwrap_or_default()),
114 htmlize::escape_attribute(link_txt),
115 htmlize::escape_text(link_txt),
116 );
117 }
118 _ => return Cow::Borrowed(text), }
120 }
121 last_end_index = link.end();
122 }
123 linkified_text.push_str(&escaped(text.get(last_end_index..).unwrap_or_default()));
124 Cow::Owned(linkified_text)
125}
126
127pub fn ends_with_href(text: &str) -> bool {
136 let mut substr = text.trim_end();
138 match substr.as_bytes().last() {
140 Some(b'\'' | b'"') => {
141 if substr
142 .get(..substr.len().saturating_sub(1))
143 .map(|s| {
144 substr = s.trim_end();
145 substr.as_bytes().last() == Some(&b'=')
146 })
147 .unwrap_or(false)
148 {
149 substr = &substr[..substr.len().saturating_sub(1)];
150 } else {
151 return false;
152 }
153 }
154 Some(b'=') => {
155 substr = &substr[..substr.len().saturating_sub(1)];
156 }
157 _ => return false,
158 }
159
160 substr.trim_end().ends_with("href")
162}
163
164pub fn room_name_or_id(
166 room_name: Option<impl Into<String>>,
167 room_id: impl AsRef<RoomId>,
168) -> String {
169 room_name.map_or_else(
170 || format!("Room ID {}", room_id.as_ref()),
171 |name| name.into(),
172 )
173}
174
175pub fn debounce_broadcast<T: Clone + Send + 'static>(
176 mut input: broadcast::Receiver<T>,
177 duration: Duration,
178) -> mpsc::Receiver<T> {
179 let (tx, rx) = mpsc::channel(1);
180
181 tokio::spawn(async move {
182 let mut last_item: Option<T> = None;
183
184 loop {
185 tokio::select! {
186 result = input.recv() => {
187 match result {
188 Ok(item) => last_item = Some(item),
189 Err(broadcast::error::RecvError::Closed) => break,
190 Err(broadcast::error::RecvError::Lagged(i)) => {
191 eprintln!("Broadcast receiver missed {i} updates");
192 continue;
195 }
196 }
197 }
198
199 _ = sleep(duration), if last_item.is_some() => {
200 if let Some(item) = last_item.take() {
201 if tx.send(item).await.is_err() {
202 break; }
204 }
205 }
206 }
207 }
208 });
209
210 rx
211}