tauri_plugin_matrix_svelte/matrix/
utils.rs

1use 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
13/// Returns the sender's display name if available.
14///
15/// If not available, and if the `room_id` is provided, this function will
16/// submit an async request to fetch the event details.
17/// In this case, this will return the event sender's user ID as a string.
18pub 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                    // TODO: handle
28                    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
42/// Returns the user ID of the currently logged-in user, if any.
43pub 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
49/// Removes leading whitespace and HTML whitespace tags (`<p>` and `<br>`) from the given `text`.
50pub 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
68/// Looks for bare links in the given `text` and converts them into proper HTML links.
69pub 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    // A closure to escape text if it's not HTML.
77    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        // Only linkify the URL if it's not already part of an HTML href attribute.
90        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), // unreachable
119            }
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
127/// Returns true if the given `text` string ends with a valid href attribute opener.
128///
129/// An href attribute looks like this: `href="http://example.com"`,.
130/// so we look for `href="` at the end of the given string.
131///
132/// Spaces are allowed to exist in between the `href`, `=`, and `"`.
133/// In addition, the quotation mark is optional, and can be either a single or double quote,
134/// so this function takes those into account as well.
135pub fn ends_with_href(text: &str) -> bool {
136    // let mut idx = text.len().saturating_sub(1);
137    let mut substr = text.trim_end();
138    // Search backwards for a single quote, double quote, or an equals sign.
139    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    // Now we have found the equals sign, so search backwards for the `href` attribute.
161    substr.trim_end().ends_with("href")
162}
163
164/// Returns a string representation of the room name or ID.
165pub 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                            // Handle lagged receiver - you might want to log this
193                            // The receiver was too slow and missed some messages
194                            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; // Receiver dropped
203                        }
204                    }
205                }
206            }
207        }
208    });
209
210    rx
211}