mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-06 15:04:11 -04:00
feat(ui): expose a method for checking whether a message contains only emojis and should be boosted (use a bigger font size) (#4577)
- supports only text room message types - enumerates through their body's grapheme clusters and check that every single one of them is an emoji - part of the `LazyTimelineItemProvider` so that it can be opt in
This commit is contained in:
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -1410,6 +1410,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emojis"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4"
|
||||
dependencies = [
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
@@ -3505,6 +3514,7 @@ dependencies = [
|
||||
"async-stream",
|
||||
"async_cell",
|
||||
"chrono",
|
||||
"emojis",
|
||||
"eyeball",
|
||||
"eyeball-im",
|
||||
"eyeball-im-util",
|
||||
@@ -3531,6 +3541,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"unicode-normalization",
|
||||
"unicode-segmentation",
|
||||
"uniffi",
|
||||
"wiremock",
|
||||
]
|
||||
@@ -6149,9 +6160,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
|
||||
@@ -1322,4 +1322,8 @@ impl LazyTimelineItemProvider {
|
||||
fn get_send_handle(&self) -> Option<Arc<SendHandle>> {
|
||||
self.0.local_echo_send_handle().map(|handle| Arc::new(SendHandle::new(handle)))
|
||||
}
|
||||
|
||||
fn contains_only_emojis(&self) -> bool {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ tracing = { workspace = true, features = ["attributes"] }
|
||||
unicode-normalization = { workspace = true }
|
||||
uniffi = { workspace = true, optional = true }
|
||||
|
||||
emojis = "0.6.4"
|
||||
unicode-segmentation = "1.12.0"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
assert-json-diff = { workspace = true }
|
||||
|
||||
@@ -36,6 +36,7 @@ use ruma::{
|
||||
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
|
||||
};
|
||||
use tracing::warn;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
mod content;
|
||||
mod local;
|
||||
@@ -604,6 +605,69 @@ impl EventTimelineItem {
|
||||
pub fn local_echo_send_handle(&self) -> Option<SendHandle> {
|
||||
as_variant!(self.handle(), TimelineItemHandle::Local(handle) => handle.clone())
|
||||
}
|
||||
|
||||
/// Some clients may want to know if a particular text message or media
|
||||
/// caption contains only emojis so that they can render them bigger for
|
||||
/// added effect.
|
||||
///
|
||||
/// This function provides that feature with the following
|
||||
/// behavior/limitations:
|
||||
/// - ignores leading and trailing white spaces
|
||||
/// - fails texts bigger than 5 graphemes for performance reasons
|
||||
/// - checks the body only for [`MessageType::Text`]
|
||||
/// - only checks the caption for [`MessageType::Audio`],
|
||||
/// [`MessageType::File`], [`MessageType::Image`], and
|
||||
/// [`MessageType::Video`] if present
|
||||
/// - all other message types will not match
|
||||
///
|
||||
/// # Examples
|
||||
/// # fn render_timeline_item(timeline_item: TimelineItem) {
|
||||
/// if timeline_item.contains_only_emojis() {
|
||||
/// // e.g. increase the font size
|
||||
/// }
|
||||
/// # }
|
||||
///
|
||||
/// See `test_emoji_detection` for more examples.
|
||||
pub fn contains_only_emojis(&self) -> bool {
|
||||
let body = match self.content() {
|
||||
TimelineItemContent::Message(msg) => match msg.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,
|
||||
},
|
||||
TimelineItemContent::RedactedMessage
|
||||
| TimelineItemContent::Sticker(_)
|
||||
| TimelineItemContent::UnableToDecrypt(_)
|
||||
| TimelineItemContent::MembershipChange(_)
|
||||
| TimelineItemContent::ProfileChange(_)
|
||||
| TimelineItemContent::OtherState(_)
|
||||
| TimelineItemContent::FailedToParseMessageLike { .. }
|
||||
| TimelineItemContent::FailedToParseState { .. }
|
||||
| TimelineItemContent::Poll(_)
|
||||
| TimelineItemContent::CallInvite
|
||||
| TimelineItemContent::CallNotify => None,
|
||||
};
|
||||
|
||||
if let Some(body) = body {
|
||||
// Collect the graphemes after trimming white spaces.
|
||||
let graphemes = body.trim().graphemes(true).collect::<Vec<&str>>();
|
||||
|
||||
// Limit the check to 5 graphemes for performance and security
|
||||
// reasons. This will probably be used for every new message so we
|
||||
// want it to be fast and we don't want to allow a DoS attack by
|
||||
// sending a huge message.
|
||||
if graphemes.len() > 5 {
|
||||
return false;
|
||||
}
|
||||
|
||||
graphemes.iter().all(|g| emojis::get(g).is_some())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
|
||||
@@ -1060,6 +1124,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_emoji_detection() {
|
||||
let room_id = room_id!("!q:x.uk");
|
||||
let user_id = user_id!("@t:o.uk");
|
||||
let client = logged_in_client(None).await;
|
||||
|
||||
let mut event = message_event(room_id, user_id, "🤷♂️ No boost 🤷♂️", "", 0);
|
||||
let mut timeline_item =
|
||||
EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!timeline_item.contains_only_emojis());
|
||||
|
||||
// Ignores leading and trailing white spaces
|
||||
event = message_event(room_id, user_id, " 🚀 ", "", 0);
|
||||
timeline_item =
|
||||
EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(timeline_item.contains_only_emojis());
|
||||
|
||||
// Too many
|
||||
event = message_event(room_id, user_id, "👨👩👦1️⃣🚀👳🏾♂️🪩👍👍🏻🫱🏼🫲🏾🙂👋", "", 0);
|
||||
timeline_item =
|
||||
EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!timeline_item.contains_only_emojis());
|
||||
|
||||
// Works with combined emojis
|
||||
event = message_event(room_id, user_id, "👨👩👦1️⃣👳🏾♂️👍🏻🫱🏼🫲🏾", "", 0);
|
||||
timeline_item =
|
||||
EventTimelineItem::from_latest_event(client.clone(), room_id, LatestEvent::new(event))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(timeline_item.contains_only_emojis());
|
||||
}
|
||||
|
||||
fn member_event(
|
||||
room_id: &RoomId,
|
||||
user_id: &UserId,
|
||||
|
||||
Reference in New Issue
Block a user