feat(sdk): Allow adding and retrieving recent emojis
Include some tests.
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user