feat(sdk): Allow adding and retrieving recent emojis

Include some tests.
This commit is contained in:
Jorge Martín
2025-09-09 10:22:13 +02:00
parent eee1fa2b71
commit 89fd0b5e53
4 changed files with 154 additions and 3 deletions
+2
View File
@@ -73,6 +73,8 @@ unstable-msc4274 = ["ruma/unstable-msc4274", "matrix-sdk-base/unstable-msc4274"]
experimental-search = ["matrix-sdk-search"]
element-recent-emojis = ["matrix-sdk-base/element-recent-emojis"]
[dependencies]
anyhow = { workspace = true, optional = true }
anymap2 = "0.13.0"
+90
View File
@@ -28,6 +28,8 @@ use eyeball::{SharedObservable, Subscriber};
use eyeball_im::{Vector, VectorDiff};
use futures_core::Stream;
use futures_util::StreamExt;
#[cfg(feature = "element-recent-emojis")]
use js_int::uint;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_base::crypto::{store::LockableCryptoStore, DecryptionSettings};
use matrix_sdk_base::{
@@ -38,7 +40,11 @@ use matrix_sdk_base::{
BaseClient, RoomInfoNotableUpdate, RoomState, RoomStateFilter, SendOutsideWasm, SessionMeta,
StateStoreDataKey, StateStoreDataValue, SyncOutsideWasm, ThreadingSupport,
};
#[cfg(feature = "element-recent-emojis")]
use matrix_sdk_base::{recent_emojis::RecentEmojisContent, store::StateStoreExt};
use matrix_sdk_common::ttl_cache::TtlCache;
#[cfg(feature = "element-recent-emojis")]
use ruma::api::client::config::set_global_account_data::v3::Request as UpdateGlobalAccountDataRequest;
#[cfg(feature = "e2e-encryption")]
use ruma::events::{room::encryption::RoomEncryptionEventContent, InitialStateEvent};
use ruma::{
@@ -2943,6 +2949,50 @@ impl Client {
pub(crate) fn thread_subscription_catchup(&self) -> &ThreadSubscriptionCatchup {
self.inner.thread_subscription_catchup.get().unwrap()
}
#[cfg(feature = "element-recent-emojis")]
async fn get_recent_emoji_content(&self) -> Result<Option<RecentEmojisContent>> {
self.state_store()
.get_account_data_event_static::<RecentEmojisContent>()
.await?
.map(|raw| raw.deserialize().map(|event| event.content))
.transpose()
.map_err(Into::into)
}
#[cfg(feature = "element-recent-emojis")]
/// Adds a recently used emoji to the list and uploads the updated
/// `io.element.recent_emoji` content to the global account data.
pub async fn add_recent_emoji(&self, emoji: &str) -> Result<()> {
let mut content =
self.get_recent_emoji_content().await?.unwrap_or_else(RecentEmojisContent::default);
let index = content.recent_emoji.iter().position(|(unicode, _)| unicode == emoji);
let count =
if let Some(index) = index { content.recent_emoji.remove(index).1 } else { uint!(0) };
content.recent_emoji.insert(0, (emoji.to_owned(), count + uint!(1)));
let Some(session_id) = self.user_id() else {
return Err(Error::AuthenticationRequired);
};
let request = UpdateGlobalAccountDataRequest::new(session_id.to_owned(), &content)?;
let _ = self.send(request).await?;
Ok(())
}
#[cfg(feature = "element-recent-emojis")]
/// Gets the list of recently used emojis from the `io.element.recent_emoji`
/// global account data.
pub async fn get_recent_emojis(&self) -> Result<Vec<(String, u64)>> {
let Some(content) = self.get_recent_emoji_content().await? else {
return Ok(Vec::new());
};
Ok(content.recent_emoji.into_iter().map(|(emoji, count)| (emoji, count.into())).collect())
}
}
#[cfg(any(feature = "testing", test))]
@@ -3074,6 +3124,10 @@ pub(crate) mod tests {
#[cfg(target_family = "wasm")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[cfg(feature = "element-recent-emojis")]
use matrix_sdk_base::recent_emojis::RecentEmojisContent;
#[cfg(feature = "element-recent-emojis")]
use ruma::events::GlobalAccountDataEventContent;
use ruma::{
api::{
client::{
@@ -4073,4 +4127,40 @@ pub(crate) mod tests {
.await
.unwrap();
}
#[cfg(feature = "element-recent-emojis")]
#[async_test]
async fn test_recent_emojis() {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
server
.mock_update_global_account_data()
.ok(client.user_id().expect("session_id"), RecentEmojisContent::default().event_type())
.named("Update recent emojis global account data")
.mock_once()
.mount()
.await;
let recent_emojis = client.get_recent_emojis().await.expect("recent emojis");
assert!(recent_emojis.is_empty());
client.add_recent_emoji(":)").await.expect("adding emoji");
server
.mock_sync()
.ok(|builder| {
let content = RecentEmojisContent::new(vec![(":)".to_owned(), uint!(1))]);
let event_builder = EventFactory::new().global_account_data(content);
builder.add_global_account_data(event_builder);
})
.named("Sync")
.mount()
.await;
client.sync_once(SyncSettings::default()).await.expect("sync failed");
let recent_emojis = client.get_recent_emojis().await.expect("recent emojis");
assert_eq!(recent_emojis.len(), 1);
}
}
@@ -1211,6 +1211,40 @@ impl MatrixMockServer {
self.mock_endpoint(mock, GlobalAccountDataEndpoint).expect_default_access_token()
}
/// Create a prebuilt mock for the endpoint that updates the global account
/// data.
///
/// # Examples
///
/// ```
/// use ruma::__private_macros::user_id;
/// tokio_test::block_on(async {
/// use matrix_sdk::test_utils::mocks::MatrixMockServer;
/// use serde_json::json;
/// use ruma::events::media_preview_config::MediaPreviews;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_update_global_account_data().ok(
/// client.user_id().unwrap(),
/// ruma::events::GlobalAccountDataEventType::IgnoredUserList,
/// )
/// .mock_once()
/// .mount()
/// .await;
///
/// client.account().ignore_user(user_id!("@a:b.c")).await.unwrap();
///
/// # anyhow::Ok(()) });
/// ```
pub fn mock_update_global_account_data(
&self,
) -> MockEndpoint<'_, UpdateGlobalAccountDataEndpoint> {
let mock = Mock::given(method("PUT"));
self.mock_endpoint(mock, UpdateGlobalAccountDataEndpoint).expect_default_access_token()
}
/// Create a prebuilt mock for the endpoint used to send a single receipt.
pub fn mock_send_receipt(
&self,
@@ -3506,6 +3540,22 @@ impl<'a> MockEndpoint<'a, GlobalAccountDataEndpoint> {
}
}
/// A prebuilt mock for the update global account data endpoint.
pub struct UpdateGlobalAccountDataEndpoint;
impl<'a> MockEndpoint<'a, UpdateGlobalAccountDataEndpoint> {
/// Returns a mock for a successful global account data event.
pub fn ok(self, user_id: &UserId, event_type: GlobalAccountDataEventType) -> MatrixMock<'a> {
let mock = self
.mock
.and(path_regex(format!(
r"^/_matrix/client/v3/user/{user_id}/account_data/{event_type}"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(()));
MatrixMock { server: self.server, mock }
}
}
/// A response to a [`RoomRelationsEndpoint`] query.
#[derive(Default)]
pub struct RoomRelationsResponseTemplate {
+12 -3
View File
@@ -29,9 +29,9 @@ use ruma::{
TransactionId, UInt, UserId, VoipVersionId,
events::{
AnyGlobalAccountDataEvent, AnyStateEvent, AnySyncMessageLikeEvent, AnySyncStateEvent,
AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, False, Mentions,
RedactedMessageLikeEventContent, RedactedStateEventContent, StateEventContent,
StaticEventContent,
AnySyncTimelineEvent, AnyTimelineEvent, BundledMessageLikeRelations, False,
GlobalAccountDataEventContent, Mentions, RedactedMessageLikeEventContent,
RedactedStateEventContent, StateEventContent, StaticEventContent,
beacon::BeaconEventContent,
call::{
SessionDescription,
@@ -1076,6 +1076,15 @@ impl EventFactory {
pub fn set_next_ts(&self, value: u64) {
self.next_ts.store(value, SeqCst);
}
pub fn global_account_data<C>(&self, content: C) -> EventBuilder<C>
where
C: GlobalAccountDataEventContent + StaticEventContent<IsPrefix = False>,
{
let mut event = self.event(content);
event.is_global = true;
event
}
}
impl EventBuilder<DirectEventContent> {