Repository: matrix-org/matrix-rust-sdk
Pull request #5062: feat(bindings): added APIs to get the media preview config from the store
Author: login: Velin92
Created: 2025-05-19T15:58:41Z | Merged: 2025-05-21T10:15:17Z

The reason behind this PR, is to extend https://github.com/matrix-org/matrix-rust-sdk/pull/5040 
While the observable value is very useful in the context of the main app, it may be problematic in the context of parallel processes that aim to aid the main app process, like for example the Notification Service Extension on iOS, which is treated as a different process, and has some very harsh memory contraints, and a limited lifetime, which would make the observable values not quite useful.

Also while we do have a [`get_media_preview_config_event_content`](https://github.com/matrix-org/matrix-rust-sdk/blob/0cfb4b1c55dd2ff8e90a6bb804944592ea819222/crates/matrix-sdk/src/account.rs#L1067) that is used by the observable API, this sends a request to the server to check for the current account data value of such event, and in the context of the NSE which already waits for a sync, we probably do not want to slow it down with another request to the server (not mentioning that sending a request for each notification might be also problematic for the servers).
For the NSE scenario I think getting whatever is currently in the store would be fine.


Diff:
diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs
index 2a9872f9f82..78ef047d274 100644
--- a/bindings/matrix-sdk-ffi/src/client.rs
+++ b/bindings/matrix-sdk-ffi/src/client.rs
@@ -1401,6 +1401,13 @@ impl Client {
     }
 
     /// Get the media previews timeline display policy
+    /// currently stored in the cache.
+    pub async fn get_media_preview_display_policy(&self) -> Result<MediaPreviews, ClientError> {
+        let configuration = self.inner.account().get_media_preview_config_event_content().await?;
+        Ok(configuration.media_previews.into())
+    }
+
+    /// Set the invite request avatars display policy
     pub async fn set_invite_avatars_display_policy(
         &self,
         policy: InviteAvatars,
@@ -1408,6 +1415,13 @@ impl Client {
         self.inner.account().set_invite_avatars_display_policy(policy.into()).await?;
         Ok(())
     }
+
+    /// Get the invite request avatars display policy
+    /// currently stored in the cache.
+    pub async fn get_invite_avatars_display_policy(&self) -> Result<InviteAvatars, ClientError> {
+        let configuration = self.inner.account().get_media_preview_config_event_content().await?;
+        Ok(configuration.invite_avatars.into())
+    }
 }
 
 #[matrix_sdk_ffi_macros::export(callback_interface)]
diff --git a/crates/matrix-sdk/src/account.rs b/crates/matrix-sdk/src/account.rs
index 85264b60a1c..d4c5bd6b603 100644
--- a/crates/matrix-sdk/src/account.rs
+++ b/crates/matrix-sdk/src/account.rs
@@ -1064,7 +1064,11 @@ impl Account {
         Ok((initial_value, result_stream))
     }
 
-    async fn get_media_preview_config_event_content(
+    /// Fetch the media preview configuration event content from the server.
+    ///
+    /// If no value was found in either the stable and unstable type, the
+    /// default configuration will be returned.
+    pub async fn fetch_media_preview_config_event_content(
         &self,
     ) -> Result<MediaPreviewConfigEventContent> {
         // First we check if there is avalue in the stable event
@@ -1087,12 +1091,36 @@ impl Account {
         Ok(media_preview_config)
     }
 
+    /// Get the media preview configuration event content stored in the cache.
+    ///
+    /// This will return the default configuration if no value was found.
+    /// Will check first for the stable event and then for the unstable one.
+    pub async fn get_media_preview_config_event_content(
+        &self,
+    ) -> Result<MediaPreviewConfigEventContent> {
+        let media_preview_config = self
+            .account_data::<MediaPreviewConfigEventContent>()
+            .await?
+            .and_then(|r| r.deserialize().ok());
+
+        let media_preview_config = if let Some(media_preview_config) = media_preview_config {
+            media_preview_config
+        } else {
+            self.account_data::<UnstableMediaPreviewConfigEventContent>()
+                .await?
+                .and_then(|r| r.deserialize().ok())
+                .unwrap_or_default()
+                .into()
+        };
+        Ok(media_preview_config)
+    }
+
     /// Set the media previews display policy in the timeline.
     ///
     /// This will always use the unstable event until we know which Matrix
     /// version will support it.
     pub async fn set_media_previews_display_policy(&self, policy: MediaPreviews) -> Result<()> {
-        let mut media_preview_config = self.get_media_preview_config_event_content().await?;
+        let mut media_preview_config = self.fetch_media_preview_config_event_content().await?;
         media_preview_config.media_previews = policy;
 
         // Updating the unstable account data
@@ -1107,7 +1135,7 @@ impl Account {
     /// This will always use the unstable event until we know which matrix
     /// version will support it.
     pub async fn set_invite_avatars_display_policy(&self, policy: InviteAvatars) -> Result<()> {
-        let mut media_preview_config = self.get_media_preview_config_event_content().await?;
+        let mut media_preview_config = self.fetch_media_preview_config_event_content().await?;
         media_preview_config.invite_avatars = policy;
 
         // Updating the unstable account data
diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs
index e39719e2c4c..c07995c53d2 100644
--- a/crates/matrix-sdk/src/client/mod.rs
+++ b/crates/matrix-sdk/src/client/mod.rs
@@ -3341,16 +3341,16 @@ pub(crate) mod tests {
         let client = server.client_builder().build().await;
 
         server
-            .mock_global_account_data()
-            .ok(
-                client.user_id().unwrap(),
-                ruma::events::GlobalAccountDataEventType::MediaPreviewConfig,
-                json!({
+            .mock_sync()
+            .ok_and_run(&client, |builder| {
+                builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom(json!({
+                "content": {
                     "media_previews": "private",
                     "invite_avatars": "off"
-                }),
-            )
-            .mount()
+                },
+                "type": "m.media_preview_config"
+                  })));
+            })
             .await;
 
         let (initial_value, stream) =
@@ -3391,25 +3391,16 @@ pub(crate) mod tests {
         let client = server.client_builder().build().await;
 
         server
-            .mock_global_account_data()
-            .not_found(
-                client.user_id().unwrap(),
-                ruma::events::GlobalAccountDataEventType::MediaPreviewConfig,
-            )
-            .mount()
-            .await;
-
-        server
-            .mock_global_account_data()
-            .ok(
-                client.user_id().unwrap(),
-                ruma::events::GlobalAccountDataEventType::UnstableMediaPreviewConfig,
-                json!({
+            .mock_sync()
+            .ok_and_run(&client, |builder| {
+                builder.add_global_account_data_event(GlobalAccountDataTestEvent::Custom(json!({
+                "content": {
                     "media_previews": "private",
                     "invite_avatars": "off"
-                }),
-            )
-            .mount()
+                },
+                "type": "io.element.msc4278.media_preview_config"
+                  })));
+            })
             .await;
 
         let (initial_value, stream) =
@@ -3449,24 +3440,6 @@ pub(crate) mod tests {
         let server = MatrixMockServer::new().await;
         let client = server.client_builder().build().await;
 
-        server
-            .mock_global_account_data()
-            .not_found(
-                client.user_id().unwrap(),
-                ruma::events::GlobalAccountDataEventType::MediaPreviewConfig,
-            )
-            .mount()
-            .await;
-
-        server
-            .mock_global_account_data()
-            .not_found(
-                client.user_id().unwrap(),
-                ruma::events::GlobalAccountDataEventType::UnstableMediaPreviewConfig,
-            )
-            .mount()
-            .await;
-
         let (initial_value, _) = client.account().observe_media_preview_config().await.unwrap();
 
         assert_eq!(initial_value.invite_avatars, InviteAvatars::On);
diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs
index 055eaa6e125..b5743c2e601 100644
--- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs
+++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs
@@ -1026,6 +1026,7 @@ impl MatrixMockServer {
     /// 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;
@@ -1042,7 +1043,7 @@ impl MatrixMockServer {
     /// .mount()
     /// .await;
     ///
-    /// let (_, _) = client.account().observe_media_preview_config().await.unwrap();
+    /// client.account().fetch_media_preview_config_event_content().await.unwrap();
     ///
     /// # anyhow::Ok(()) });
     /// ```


Reviews:
user: login: zecakeh
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T09:23:35Z
user: login: poljar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T09:45:54Z
user: login: Velin92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T11:33:03Z
user: login: Velin92
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T11:41:01Z
user: login: poljar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:00:01Z
user: login: poljar
body: Looks good, let's just rename the methods a bit.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T09:17:31Z
user: login: poljar
body: Thanks, looking good.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T10:09:45Z

Review comments:
diff_hunk: @@ -1087,6 +1087,30 @@ impl Account {
         Ok(media_preview_config)
     }
 
+    /// Get the media preview configuration event content stored in the cache.
+    ///
+    /// This will return the default configuration if no value was found.
+    /// Will check first for the stable event and then for the unstable one.
+    pub async fn get_stored_media_preview_config_event_content(
path: crates/matrix-sdk/src/account.rs
user: login: zecakeh
body: Could `observe_media_preview_config` use this method to get the initial value, or is there a reason that it must make a request to the homeserver?
created_at: 2025-05-20T09:23:35Z
author_association: COLLABORATOR
diff_hunk: @@ -1087,6 +1087,30 @@ impl Account {
         Ok(media_preview_config)
     }
 
+    /// Get the media preview configuration event content stored in the cache.
+    ///
+    /// This will return the default configuration if no value was found.
+    /// Will check first for the stable event and then for the unstable one.
+    pub async fn get_stored_media_preview_config_event_content(
path: crates/matrix-sdk/src/account.rs
user: login: poljar
body: Well, historically that has been a bit of a footgun because the sliding sync proxy tended to be a bit slow to sync the account data.

So you would get a stale value. I suppose it's not that bad to get a stale value here since you're going to receive the update eventually.

In conclusion, yeah I think you're right.
created_at: 2025-05-20T09:45:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1087,6 +1087,30 @@ impl Account {
         Ok(media_preview_config)
     }
 
+    /// Get the media preview configuration event content stored in the cache.
+    ///
+    /// This will return the default configuration if no value was found.
+    /// Will check first for the stable event and then for the unstable one.
+    pub async fn get_stored_media_preview_config_event_content(
path: crates/matrix-sdk/src/account.rs
user: login: Velin92
body: okay perfect then I'll rework the existing code to use this in place of the API call, the only problem is that probably I'll need to update the tests
created_at: 2025-05-20T11:33:03Z
author_association: MEMBER
diff_hunk: @@ -1087,6 +1087,30 @@ impl Account {
         Ok(media_preview_config)
     }
 
+    /// Get the media preview configuration event content stored in the cache.
+    ///
+    /// This will return the default configuration if no value was found.
+    /// Will check first for the stable event and then for the unstable one.
+    pub async fn get_stored_media_preview_config_event_content(
path: crates/matrix-sdk/src/account.rs
user: login: Velin92
body: Another question should I also do the same on the setters?
https://github.com/matrix-org/matrix-rust-sdk/blob/0cfb4b1c55dd2ff8e90a6bb804944592ea819222/crates/matrix-sdk/src/account.rs#L1114-L1142

Or the setters should keep fetching the API directly?
created_at: 2025-05-20T11:41:01Z
author_association: MEMBER
diff_hunk: @@ -1087,6 +1087,30 @@ impl Account {
         Ok(media_preview_config)
     }
 
+    /// Get the media preview configuration event content stored in the cache.
+    ///
+    /// This will return the default configuration if no value was found.
+    /// Will check first for the stable event and then for the unstable one.
+    pub async fn get_stored_media_preview_config_event_content(
path: crates/matrix-sdk/src/account.rs
user: login: poljar
body: Answered in the chat room, but for completeness sake. The setters should still send out the request, in case the sync is late or not running, so we don't edit stale data.
created_at: 2025-05-20T14:00:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1401,13 +1401,33 @@ impl Client {
     }
 
     /// Get the media previews timeline display policy
+    /// currently stored in the cache.
+    pub async fn get_stored_media_preview_display_policy(
path: bindings/matrix-sdk-ffi/src/client.rs
user: login: poljar
body: The `stored` is a bit rendundant here. We have a kinda soft policy that we call functions that get things from the store `get_foo()` while we call functions that get things from the server `fetch_foo()`.

```suggestion
    pub async fn get_media_preview_display_policy(
```
created_at: 2025-05-21T09:15:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1059,12 +1059,16 @@ impl Account {
         // We need to get the initial value of the media preview config event
         // we do this after creating the observers to make sure that we don't
         // create a race condition
-        let initial_value = self.get_media_preview_config_event_content().await?;
+        let initial_value = self.get_stored_media_preview_config_event_content().await?;
 
         Ok((initial_value, result_stream))
     }
 
-    async fn get_media_preview_config_event_content(
+    /// Get the media preview configuration event content from the server.
+    ///
+    /// If no value was found in either the stable and unstable type, the
+    /// default configuration will be returned.
+    pub async fn get_media_preview_config_event_content(
path: crates/matrix-sdk/src/account.rs
user: login: poljar
body: Now that we have two methods, let's call this one `fetch_...()` and the one that access the store `get()`, to avoid the overly long name.
created_at: 2025-05-21T09:17:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: Velin92
created_at: 2025-05-19T16:07:44Z
body: @poljar I wonder if we shouldn't actually use also in the [observable API](https://github.com/matrix-org/matrix-rust-sdk/blob/0cfb4b1c55dd2ff8e90a6bb804944592ea819222/crates/matrix-sdk/src/account.rs#L1026-L1088) the currently stored value, instead of doing a request to the server... WDYT?
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-05-19T16:13:06Z
body: ## [Codecov](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5062?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org) Report
Attention: Patch coverage is `76.92308%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 85.75%. Comparing base [(`d462683`)](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/commit/d4626835bb21431346b77b317abf847d86ac16de?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org) to head [(`f8ae8fb`)](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/commit/f8ae8fbec4903052fbca95017c14f32e0eb15f38?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org).
> Report is 23 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5062?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org) | Patch % | Lines |
|---|---|---|
| [crates/matrix-sdk/src/account.rs](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5062?src=pr&el=tree&filepath=crates%2Fmatrix-sdk%2Fsrc%2Faccount.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org#diff-Y3JhdGVzL21hdHJpeC1zZGsvc3JjL2FjY291bnQucnM=) | 76.92% | [3 Missing :warning: ](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5062?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #5062      +/-   ##
==========================================
- Coverage   85.81%   85.75%   -0.06%     
==========================================
  Files         325      325              
  Lines       35922    35932      +10     
==========================================
- Hits        30826    30815      -11     
- Misses       5096     5117      +21     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/matrix-org/matrix-rust-sdk/pull/5062?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=matrix-org).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: poljar
created_at: 2025-05-20T10:09:08Z
body: > @poljar I wonder if we shouldn't actually use also in the [observable API](https://github.com/matrix-org/matrix-rust-sdk/blob/0cfb4b1c55dd2ff8e90a6bb804944592ea819222/crates/matrix-sdk/src/account.rs#L1026-L1088) the currently stored value, instead of doing a request to the server... WDYT?

As mentioned here: https://github.com/matrix-org/matrix-rust-sdk/pull/5062#discussion_r2097470482. Yes we can do that.
author_association: CONTRIBUTOR