Skip to content

Commit

Permalink
feat(ui): expose a method for checking whether a message contains onl…
Browse files Browse the repository at this point in the history
…y emojis and should be boosted (use a bigger font size) (matrix-org#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
  • Loading branch information
stefanceriu authored Jan 27, 2025
1 parent aaecbf0 commit 2657eb7
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 2 deletions.
15 changes: 13 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions bindings/matrix-sdk-ffi/src/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
3 changes: 3 additions & 0 deletions crates/matrix-sdk-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
106 changes: 106 additions & 0 deletions crates/matrix-sdk-ui/src/timeline/event_item/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use ruma::{
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
};
use tracing::warn;
use unicode_segmentation::UnicodeSegmentation;

mod content;
mod local;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 2657eb7

Please sign in to comment.