use std::borrow::Cow;
use tokio::sync::{broadcast, mpsc};
use tokio::time::{Duration, sleep};
use tracing::warn;
use matrix_sdk::ruma::RoomId;
use matrix_sdk_ui::timeline::{EventTimelineItem, TimelineDetails};
use crate::events::timeline::TimelineKind;
use crate::models::async_requests::{MatrixRequest, submit_async_request};
use crate::models::events::DeviceGuessedType;
pub fn get_or_fetch_event_sender(
event_tl_item: &EventTimelineItem,
timeline_kind: Option<TimelineKind>,
) -> String {
let sender_username = match event_tl_item.sender_profile() {
TimelineDetails::Ready(profile) => profile.display_name.as_deref(),
TimelineDetails::Unavailable => {
if let Some(timeline_kind) = timeline_kind
&& let Some(event_id) = event_tl_item.event_id()
{
submit_async_request(MatrixRequest::FetchDetailsForEvent {
timeline_kind,
event_id: event_id.to_owned(),
});
}
None
}
_ => None,
}
.unwrap_or_else(|| event_tl_item.sender().as_str());
sender_username.to_owned()
}
pub fn trim_start_html_whitespace(mut text: &str) -> &str {
let mut prev_text_len = text.len();
loop {
text = text
.trim_start_matches("<p>")
.trim_start_matches("<br>")
.trim_start_matches("<br/>")
.trim_start_matches("<br />")
.trim_start();
if text.len() == prev_text_len {
break;
}
prev_text_len = text.len();
}
text
}
pub fn linkify(text: &str, is_html: bool) -> Cow<'_, str> {
use linkify::{LinkFinder, LinkKind};
let mut links = LinkFinder::new().links(text).peekable();
if links.peek().is_none() {
return Cow::Borrowed(text);
}
let escaped = |text| {
if is_html {
Cow::from(text)
} else {
htmlize::escape_text(text)
}
};
let mut linkified_text = String::new();
let mut last_end_index = 0;
for link in links {
let link_txt = link.as_str();
let is_link_within_href_attr = text.get(..link.start()).is_some_and(ends_with_href);
let is_link_within_html_tag = text
.get(link.end()..)
.is_some_and(|after| after.trim_end().starts_with("</a>"));
if is_link_within_href_attr || is_link_within_html_tag {
linkified_text = format!(
"{linkified_text}{}",
text.get(last_end_index..link.end()).unwrap_or_default(),
);
} else {
match link.kind() {
LinkKind::Url => {
linkified_text = format!(
"{linkified_text}{}<a href=\"{}\">{}</a>",
escaped(text.get(last_end_index..link.start()).unwrap_or_default()),
htmlize::escape_attribute(link_txt),
htmlize::escape_text(link_txt),
);
}
LinkKind::Email => {
linkified_text = format!(
"{linkified_text}{}<a href=\"mailto:{}\">{}</a>",
escaped(text.get(last_end_index..link.start()).unwrap_or_default()),
htmlize::escape_attribute(link_txt),
htmlize::escape_text(link_txt),
);
}
_ => return Cow::Borrowed(text), }
}
last_end_index = link.end();
}
linkified_text.push_str(&escaped(text.get(last_end_index..).unwrap_or_default()));
Cow::Owned(linkified_text)
}
pub fn ends_with_href(text: &str) -> bool {
let mut substr = text.trim_end();
match substr.as_bytes().last() {
Some(b'\'' | b'"')
if substr
.get(..substr.len().saturating_sub(1))
.map(|s| {
substr = s.trim_end();
substr.as_bytes().last() == Some(&b'=')
})
.unwrap_or(false) =>
{
substr = &substr[..substr.len().saturating_sub(1)];
}
Some(b'=') => {
substr = &substr[..substr.len().saturating_sub(1)];
}
_ => return false,
}
substr.trim_end().ends_with("href")
}
pub fn room_name_or_id(
room_name: Option<impl Into<String>>,
room_id: impl AsRef<RoomId>,
) -> String {
room_name.map_or_else(
|| format!("Room ID {}", room_id.as_ref()),
|name| name.into(),
)
}
pub fn debounce_broadcast<T: Clone + Send + 'static>(
mut input: broadcast::Receiver<T>,
duration: Duration,
) -> mpsc::Receiver<T> {
let (tx, rx) = mpsc::channel(1);
tokio::spawn(async move {
let mut last_item: Option<T> = None;
loop {
tokio::select! {
result = input.recv() => {
match result {
Ok(item) => last_item = Some(item),
Err(broadcast::error::RecvError::Closed) => break,
Err(broadcast::error::RecvError::Lagged(i)) => {
warn!("Broadcast receiver missed {i} updates");
continue;
}
}
}
_ = sleep(duration), if last_item.is_some() => {
if let Some(item) = last_item.take() && tx.send(item).await.is_err() {
break; }
}
}
}
});
rx
}
pub(crate) fn guess_device_type(display_name: Option<&str>) -> DeviceGuessedType {
let Some(display_name) = display_name else {
return DeviceGuessedType::Unknown;
};
let display_lower = display_name.to_lowercase();
if display_lower.contains("ios")
|| display_lower.contains("iphone")
|| display_lower.contains("ipad")
{
DeviceGuessedType::Ios
} else if display_lower.contains("android") {
DeviceGuessedType::Android
} else if display_lower.contains("firefox")
|| display_lower.contains("chrome")
|| display_lower.contains("safari")
|| display_lower.contains("web")
{
DeviceGuessedType::Web
} else if display_lower.contains("desktop") || display_lower.contains("element desktop") {
DeviceGuessedType::Desktop
} else {
DeviceGuessedType::Unknown
}
}
#[derive(Debug)]
pub(crate) enum VecDiff<T> {
Append { values: Vec<T> },
Clear,
Insert { index: usize, value: T },
Set { index: usize, value: T },
Remove { index: usize },
PushFront { value: T },
PushBack { value: T },
PopFront,
PopBack,
Truncate { length: usize },
}