Compare commits

...

711 Commits

Author SHA1 Message Date
ganfra f92ccd8d40 feat(ffi): expose BeaconError from sdk to ffi
Benchmarks / Run Benchmarks (crypto_bench) (push) Failing after 1m29s
Benchmarks / Run Benchmarks (event_cache) (push) Failing after 6s
Benchmarks / Run Benchmarks (linked_chunk) (push) Failing after 6s
Benchmarks / Run Benchmarks (room_list) (push) Failing after 7s
Benchmarks / Run Benchmarks (store_bench) (push) Failing after 7s
Benchmarks / Run Benchmarks (timeline) (push) Failing after 7s
Bindings tests / xtask (push) Failing after 2m6s
Bindings tests / Run Complement Crypto tests (push) Failing after 0s
Rust tests / xtask (push) Failing after 42s
Rust tests / 🐧 [m], experimental-encrypted-state-events (push) Has been skipped
Rust tests / 🐧 [m], markdown (push) Has been skipped
Rust tests / 🐧 [m], no-encryption (push) Has been skipped
Rust tests / 🐧 [m], no-encryption-and-sqlite (push) Has been skipped
Rust tests / 🐧 [m], no-sqlite (push) Has been skipped
Rust tests / 🐧 [m], search (push) Has been skipped
Rust tests / 🐧 [m], socks (push) Has been skipped
Rust tests / 🐧 [m], sqlite-cryptostore (push) Has been skipped
Rust tests / 🐧 [m], sso-login (push) Has been skipped
Rust tests / 🐧 [m]-examples (push) Has been skipped
Rust tests / 🐧 [m]-crypto (push) Has been skipped
Rust tests / 🕸️ [m]-indexeddb (push) Has been skipped
Rust tests / 🕸️ [m]-base (push) Has been skipped
Rust tests / 🕸️ [m]-common (push) Has been skipped
Rust tests / 🕸️ [m], indexeddb stores (push) Has been skipped
Rust tests / 🕸️ [m], indexeddb stores, no crypto (push) Has been skipped
Rust tests / 🕸️ [m], no-default (push) Has been skipped
Rust tests / 🕸️ [m]-qrcode (push) Has been skipped
Rust tests / 🕸️ [m]-ui (push) Has been skipped
Rust tests / Lint (push) Has been skipped
Rust tests / 🐧 all crates, 🦀 beta (push) Failing after 1m4s
Rust tests / 🐧 all crates, 🦀 stable (push) Failing after 31s
Rust tests / Spell Check with Typos (push) Failing after 1m43s
Rust tests / Integration test (features: default) (push) Failing after 1m20s
Rust tests / Integration test (features: experimental-encrypted-state-events) (push) Failing after 36s
Code Coverage / xtask (push) Failing after 29s
Code Coverage / Code Coverage (push) Has been skipped
Documentation / All crates (push) Failing after 2m1s
Rust version / msrv (push) Failing after 40s
Lint GHA workflows with zizmor / Run zizmor (push) Failing after 9m53s
Bindings tests / Test UniFFI bindings generation (push) Has been skipped
Bindings tests / matrix-rust-components-kotlin (push) Has been skipped
Bindings tests / Generate Crypto FFI Apple XCFramework (push) Has been cancelled
Rust tests / 🍏 all crates, 🦀 stable (push) Has been cancelled
Bindings tests / matrix-rust-components-swift (push) Has been cancelled
2026-04-23 11:25:24 +02:00
Jorge Martín 2c2e0f1b15 doc: Add doc comments and changelog entries 2026-04-22 17:26:51 +02:00
Jorge Martín f314578df5 feat(ffi): Add Client::get_dm_rooms
Expose the new function in the FFI layer
2026-04-22 17:26:51 +02:00
Jorge Martín 739289cd82 feat(ui): Add Client::get_dm_rooms
This function returns an iterator with all the available rooms that match the behaviour previously in `Client::get_dm`. `Client::get_dm` now just calls this function and returns the first item.
2026-04-22 17:26:51 +02:00
Kévin Commaille 1d7d6c943b feat(sdk): Add an expiry to the cached homeserver capabilities
I believe that it is a footgun to cache the data indefinitely by default
so this copies the same behavior as for the supported versions in the
ClientCaches: it sets an expiry duration of 1 day and refreshes the data
in the background when it has expired.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 14:21:34 +03:00
Damir Jelić 917f9ee298 chore: Bump vodozemac 2026-04-22 12:30:38 +02:00
Andy Balaam c7d44ddad3 Change in to into
Signed-off-by: Andy Balaam <mail@artificialworlds.net>
2026-04-22 10:52:25 +01:00
Kévin Commaille 1a170ddf90 fix(common): The expiry duration of TtlValue is 1 day
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Kévin Commaille 696d1cacbe refactor(common): Don't check expiry for TtlValue::into_data()
This API is no longer in use so we can remove it. We also rename
`into_data_unchecked()` to `into_data()`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Kévin Commaille c6d1cf20b6 feat(sdk): Refresh well-known cache in the background
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-22 10:52:25 +01:00
Jonas Platte 38f34e66eb Collapse an else { if ... }
as suggested by clippy.
2026-04-22 08:52:04 +02:00
Jonas Platte 8fa411aaa0 Clean up a lint 2026-04-22 08:52:04 +02:00
Jonas Platte b4b3b5258c Clean up some imports 2026-04-22 08:52:04 +02:00
Andy Balaam fd6cb6647f Support stable prefix for MSC4287 m.key_backup 2026-04-21 14:17:26 +01:00
Andy Balaam 103ff7d3ec Use stable identifier for m.shared_history 2026-04-21 13:47:19 +01:00
Andy Balaam efa28a1ffd Use stable identifiers for m.room_key_bundle and m.history_not_shared 2026-04-21 13:47:19 +01:00
Andy Balaam c4640897d4 Test for serialization and deserialization of a room key bundle 2026-04-21 13:47:19 +01:00
Benjamin Bouvier 0487a143ed test(integration): workaround non-working subscription to make the test pass in CI 2026-04-21 14:43:55 +02:00
Benjamin Bouvier 7ab97d59b8 test(integration): add an integration test for computing latest events in a few rooms 2026-04-21 14:43:55 +02:00
Benjamin Bouvier 940862ebed chore: bump the integration testing synapse version to v1.151.0 2026-04-21 14:43:55 +02:00
Jorge Martín e507eaabf6 feat(ffi): Expose ffi::NotificationRoomInfo::service_members
This is needed in some clients to know if a direct room is a DM or not
2026-04-21 13:22:58 +02:00
Jorge Martín a2bbc33920 doc(ffi): Add changelog entry 2026-04-21 10:38:14 +01:00
Jorge Martín db787c28b8 refactor(sdk-crypto): Fix several issues related to the newly enabled experimental-push-secrets feature 2026-04-21 10:38:14 +01:00
Jorge Martín a3d6114ddd refactor(ffi): Use the shared default features for the Kotlin bindings
This should align them with iOS.
2026-04-21 10:38:14 +01:00
Jorge Martín 58a139bf96 feat(ffi): Enable experimental-push-secrets feature by default 2026-04-21 10:38:14 +01:00
Kévin Commaille 012bc03014 refactor: Use TaskMonitor::spawn_finite_task
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Kévin Commaille 38adf952bd chore: Fix typo
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Kévin Commaille 99d675d85a feat(sdk): Refresh supported versions cache in the background
The previous behavior was making the data unavailable as soon as it is
expired, forcing the user to make a request to access fresh data. This
change keeps the data after it is expired, allowing the function to
return quickly, and triggers a background task to refresh it so that
later calls will be able to use the updated data.

This is a step towards #6090. While it's not clear in the issue what
should be the trigger for the background refreshes, this implements at
least the background refresh while keeping the expiry time as a trigger.

The next steps are to apply this to other cached values and shorten the
expiry time to something like 1 day, which should be more helpful.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Kévin Commaille be5eebaa35 refactor(base): Move and rename TtlStoreValue
Its is now available as `matrix_sdk_common::ttl_cache::TtlValue`,
allowing to use it outside of a store.

It also supports deserializing data without a timestamp, to allow to
add an expiration time to data that was already persisted.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-21 11:18:43 +02:00
Benjamin Bouvier cf4d3cb492 doc(ffi): clarify when to call Client::enable_automatic_backpagination
Since the boolean is read only once when subscribing the event cache,
let's make it clear when the FFI function should be called (based on
most prominent types used at the FFI layer).
2026-04-21 10:14:52 +02:00
dependabot[bot] ad5241b2e5 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from e82b6af0d3610e62ddf49bf7c62e662d7e31db0d to ac30a525c0a8d163a92d727b3ff079ee3f6ecb08.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/e82b6af0d3610e62ddf49bf7c62e662d7e31db0d...e82b6af0d3610e62ddf49bf7c62e662d7e31db0d)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: e82b6af0d3610e62ddf49bf7c62e662d7e31db0d
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 10:13:59 +02:00
dependabot[bot] d083c56ccb chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.2 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/671740ac38dd9b0130fbe1cec585b89eea48d3de...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 10:02:08 +02:00
dependabot[bot] eb493de1b8 chore(deps): bump actions/cache from 5.0.4 to 5.0.5
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to 5.0.5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 10:01:44 +02:00
dependabot[bot] 6aa4fdaed4 chore(deps): bump actions/upload-pages-artifact from 4.0.0 to 5.0.0
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/7b1f4a764d45c48632c6b24a0339c27f5614fb0b...fc324d3547104276b827a68afc52ff2a11cc49c9)

---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:59:22 +02:00
dependabot[bot] 9c6bbd98a7 chore(deps): bump taiki-e/install-action from 2.68.26 to 2.75.10
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.26 to 2.75.10.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.68.26...85b24a67ef0c632dfefad70b9d5ce8fddb040754)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.75.10
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 09:47:28 +02:00
Benjamin Bouvier 8b14a8920c doc(sdk): add a module doc code example for search 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 98b36fe5e0 chore: update changelogs 2026-04-20 12:18:52 +02:00
Benjamin Bouvier a1f32d741f refactor(ffi): remove unused struct 2026-04-20 12:18:52 +02:00
Benjamin Bouvier ce01854078 refactor(sdk): Room::search_messages and Client::search_messages 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 7f8c9b0244 refactor(search): pass the number of results per batch in the ctors 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 3621acb445 refactor(sdk): move the high-level search helpers back to the SDK crate 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 3789374951 refactor(sdk): move search related functions to a message_search module 2026-04-20 12:18:52 +02:00
Benjamin Bouvier e54a36a1ca refactor(search): simplify global search more by avoiding is_done tracking 2026-04-20 12:18:52 +02:00
Benjamin Bouvier cf95c3f4cc refactor(search): avoid using a HashMap for linear iteration 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 6ccfc92139 refactor(search): simplify code around global search 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 4ee20befa3 chore: address first batch of review comments 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 4348d9de23 chore: add changelog entries for search! 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 477ee70e1c feat(ffi): bindings for search 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 83989c030a feat(ffi): allow enabling search with a search index store 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 717b6371f8 feat(multiverse): add support for global search (ctrl+g) 2026-04-20 12:18:52 +02:00
Benjamin Bouvier 6fa832cfc8 feat(search): higher-level Search API 2026-04-20 12:18:52 +02:00
Ivan Enderlin 1bff8cbe5d chore(ffi): Do not log errors in ClientError::from_str.
This patch stops logging errors in `ClientError::from_str` as it was an
artifact of some old debugging sessions.
2026-04-20 11:33:15 +02:00
Ivan Enderlin 9bb67e8422 fix(ffi): Use the ERROR log level for errors.
This patch changes a `warn!` to an `error!` in `ClientError`. There are
errors, let's use the proper log level here.
2026-04-20 11:33:15 +02:00
Damir Jelić c962a52f73 ci: Add a zizmor action 2026-04-20 09:49:55 +02:00
Damir Jelić 4d20b481c1 ci: Upload the coverage directly as part of the coverage workflow 2026-04-20 09:49:55 +02:00
Damir Jelić a5136c7384 ci: Add a zizmor config and use correct hashes for our github actions 2026-04-20 09:49:55 +02:00
Andy Balaam 0e120549ce Remove unneeded TODO in RoomKeyBundleContent
Quoting @poljar:

> If you follow the stack till you get where the secret key is, you end up here:
> https://github.com/ruma/ruma/blob/6282fd8838fd2c84f252f85973d8a43577ced91a/crates/ruma-events/src/room.rs#L269-L273
> And that is now zeroized.
> So yeah, the TODO was resolved in Ruma.
2026-04-17 16:18:36 +02:00
Damir Jelić 1e74c48351 fix(thread): Only apply valid edits in the thread summary 2026-04-17 12:34:47 +02:00
Damir Jelić 9e242abda9 test(latest-event): Check that we don't apply invalid edits in the latest event 2026-04-17 12:34:47 +02:00
Damir Jelić 62ff895ce8 fix(latest-event): Don't apply invalid edits on the latest event 2026-04-17 12:34:47 +02:00
Damir Jelić d0d22ece78 refactor(latest-event): Ensure we have access to the event JSON when deciding on a latest event 2026-04-17 12:34:47 +02:00
Damir Jelić d49d86e8b0 fix(ui): Follow the spec more closely in how we reject invalid edits 2026-04-17 12:34:47 +02:00
Damir Jelić b226191bf4 test(common): Add some tests to validate the edit validation logic 2026-04-17 12:34:47 +02:00
Damir Jelić 21d7cd1b9d feat(common): Add a function to validate edits 2026-04-17 12:34:47 +02:00
Ivan Enderlin b066260554 doc(ui): Add #6459 in the `CHANGELOG.md. 2026-04-17 11:02:21 +02:00
Ivan Enderlin dbc6e3c3aa fix(ui): Fix a possible panic in Switch by updating async-rx.
This patch updates `async-rx` to fix a possible panic in `Switch`. It
now requires the outer stream to implement `FusedStream` (for the moment).
2026-04-17 11:02:21 +02:00
Benoit Marty 3293c94451 feat(ffi): Expose Client.request_openid_token() in order to let sdk clients request an openId token 2026-04-17 08:29:43 +00:00
Andy Balaam 53385798f6 Changelog update for #6457 2026-04-17 09:15:58 +01:00
Andy Balaam d49736455b Discard room key if someone leaves a room for a non-leave reason e.g. ban 2026-04-17 09:15:58 +01:00
Andy Balaam e278a99a20 Factor out parts of test_history_share_on_invite_room_key_rotation 2026-04-17 09:15:58 +01:00
Andy Balaam d5c7c886a1 Add a note helping to understand that an existing test is sufficient to cover e.g. Ban events as well as Leave 2026-04-17 09:15:58 +01:00
Ivan Enderlin b9b40c0ef2 doc(sdk): Add #6453 in the `CHANGELOG.md. 2026-04-15 17:49:46 +02:00
Ivan Enderlin 99d60bcc70 fix(sdk): Fix an infinite loop when loading pinned events from the storage.
This patch fixes a critical infinite loop when loading pinned events
from the storage if and only if there is more than one chunk for the
pinned events.

The previous implementation was broken in two ways:

1. if there was no previous chunk, then `prev` was set to `None`, but
   `previous` was never updated, so it was loop forever,
2. `load_previous_chunk` was called with the `last_chunk.previous` chunk
   identifier, which is incorrect: it must be called with the last chunk
   identifier, not the previous': otherwise it skips one chunk every time.

This patch adds a test to ensure everything works as expected.
2026-04-15 17:49:46 +02:00
Damir Jelić 64d602141a chore: Bump rustls and rustls-webpki
There appears to be a security issue.
2026-04-15 15:04:02 +02:00
Floriane TUERNAL SABOTINOV 59d2f8d4af refactor(examples): update get-profiles example to use Account::fetch_user_profile
Fixes #5902

## What this PR does

Keeps the existing `Client::send()` demo but extends it with:

- A doc comment on `get_profile` explaining the spec behaviour and the 401 
condition on  (Synapse's `require_auth_for_profile_requests`)
- A `get_profile_authenticated()` fallback using `account().fetch_user_profile()` 
which internally uses `force_auth()`
2026-04-15 13:00:41 +00:00
Kévin Commaille 80a253ada9 test(sdk): Add tests for profile capabilities
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-15 13:14:27 +02:00
Kévin Commaille ec5082bb9c fix(client): Fix logic for profile fields capabilities
The current implementation assumes that if the `m.profile_fields`
capability is missing, it means that the capability is not enabled, but
this is not what the spec says. However the spec says that it depends on
the Matrix versions advertised by the homeserver. So this adds a method
that properly computes the capability depending on the supported
versions too.

Similarly, for the display name and avatar URL fields the implementation
assumes that if the `m.profile_fields` capability is present but
disabled, we should fallback to the legacy capabilities. This behavior
is not present in the spec, so the code is changed to always use the new
capability when it is present, whether it is enabled or not. The
legacy capabilities are only used if the new capability is missing and
the homeserver doesn't advertise support for extended profile fields.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-15 13:14:27 +02:00
Johannes Marbach 63f5be6935 feat(sync): Allow setting a custom Sliding Sync connection ID and timeline limit on RoomListService
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-04-15 11:06:57 +02:00
dependabot[bot] dab87b7731 chore(deps): bump CodSpeedHQ/action from 4.12.1 to 4.13.0
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.12.1 to 4.13.0.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codspeedhq/action/compare/1c8ae4843586d3ba879736b7f6b7b0c990757fab...db35df748deb45fdef0960669f57d627c1956c30)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: 4.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 16:03:20 +02:00
dependabot[bot] 63f33a081f chore(deps): bump taiki-e/install-action from 2.68.26 to 2.74.0
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.68.26 to 2.74.0.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.68.26...v2.74.0)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.74.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 15:42:51 +02:00
Damir Jelić 8b762b04df ci: Install cargo-llvm-cov explicitly for the coverage action as well 2026-04-14 15:05:28 +02:00
Ivan Enderlin a9ef675b1c chore(sdk): Move `insert_sent_event_from_send_queue. 2026-04-14 14:45:14 +02:00
Ivan Enderlin e69bb96144 test(integration): Fix the test_local_echo_to_send_event_has_encryption_info test.
This test forgot to subscribe to the Event Cache, hence the result
was partially okay. It was working by luck before. With the previous
commits, it was not possible to work without the Event Cache being
listening to the sync.
2026-04-14 14:45:14 +02:00
Ivan Enderlin 63b8fc8b47 test: Adjust tests according to previous patches.
This patch adjust tests since the Event Cache ignores events from the
Send Queue if the cache is empty. It mostly impacts the tests as this
scenario is pretty rare in real world use cases.
2026-04-14 14:45:14 +02:00
Ivan Enderlin d68879d0aa test(sdk): Test Send Queue inserts in the Event Cache as expected. 2026-04-14 14:45:14 +02:00
Ivan Enderlin 1afebd6d4e fix(sdk): Events from the Send Queue are inserted in the Event Cache if and only if it is no empty.
This patch fixes a bug where inserting an event from the Send Queue in
an empty Event Cache will break the back-pagination logic. Indeed, the
detection of the start of the timeline during the back-pagination is
conditioned to the emptiness of the cache:

- if there is no gap, and if the cache is not empty, then we consider
  the start of the timeline has been reached.

However, if an event from the Send Queue has been inserted, with no
previous batch token (because none can be computed at this step), no gap
is present and the cache won't be empty, so… this is wrongly assumed to
be the start of the timeline.

The solution to this problem is to insert the event from the Send Queue
if the cache is not empty.
2026-04-14 14:45:14 +02:00
Damir Jelić db4c1f8dbd ci: Explicitly state that we want cargo-hack installed 2026-04-14 14:44:58 +02:00
dependabot[bot] c1b89a2751 chore(deps): bump crate-ci/typos from 1.44.0 to 1.45.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.44.0 to 1.45.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/631208b7aac2daa8b707f55e7331f9112b0e062d...cf5f1c29a8ac336af8568821ec41919923b05a83)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 14:43:37 +02:00
Damir Jelić 9d39e4bae8 ci: Explicitly state which tool we want to install, namely nextest 2026-04-14 13:55:26 +02:00
Ivan Enderlin 936f173e6f doc(sdk): Add #6396 in the CHANGELOG.md. 2026-04-14 12:51:02 +02:00
Ivan Enderlin 9eb4a5a24c test(sdk): Add a test to ensure a redacted latest event is removed. 2026-04-14 12:51:02 +02:00
Ivan Enderlin 49bba4d471 test(sdk): A TODO has been resolved. 2026-04-14 12:51:02 +02:00
Ivan Enderlin f23deacec4 fix(sdk): Do not update the LatestEventValue if its event ID matches the previous value.
This patch updates the logic to update a `LatestEventValue`. We still
can't compare two `LatestEventValue` because the type doesn't implement
`PartialEq`. However, we can use the `EventId` in some case.

It fixes https://github.com/matrix-org/matrix-rust-sdk/issues/6381.
2026-04-14 12:51:02 +02:00
Jorge Martín 9c28ee7041 feat(ffi): Add ffi::Client::set_avatar_url
This method uses `Account::set_avatar_url` under the hood to update the user's avatar to the provided MXC url
2026-04-14 12:08:47 +02:00
dependabot[bot] 5d420b683f chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from b81ce1560c5fbd0210cb66d88bf210329ff04266 to e82b6af0d3610e62ddf49bf7c62e662d7e31db0d.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/b81ce1560c5fbd0210cb66d88bf210329ff04266...e82b6af0d3610e62ddf49bf7c62e662d7e31db0d)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: e82b6af0d3610e62ddf49bf7c62e662d7e31db0d
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 11:12:08 +02:00
dependabot[bot] 2d5b95e45e chore(deps): bump rand from 0.10.0 to 0.10.1
Bumps [rand](https://github.com/rust-random/rand) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.10.0...0.10.1)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.10.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-14 09:49:29 +02:00
Benjamin Bouvier 3d29db5ebb refactor(tests): simplify timeline/reactions tests to remove custom respond_with 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 8e041d6958 refactor(tests): use MatrixMockServer even more in room_list_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier c02d0f1d01 refactor(tests): make more use of the MatrixMockServer in ui tests 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 2da262e56c refactor(tests): make use of MatrixMockServer in other room_list_service tests 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 31e4657718 refactor(tests): make use of MatrixMockServer in room_list_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 5b2fe3cf47 refactor(tests): make use of MatrixMockServer in notification_client 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 49d7d4528d refactor(tests): make use of MatrixMockServer in sync_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 3d7e505530 refactor(tests): make use of MatrixMockServer in encryption_sync_service 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 56d4163982 refactor(tests): make use of MatrixMockServer in timeline/pagination 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 4cafc48cc4 refactor(tests): make use of MatrixMockServer in timeline/focus_event 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 8ea55b30bc refactor(tests): make use of MatrixMockServer in timeline/sliding_sync
We could do better, and have even a specialized sliding sync endpoint
helper, but that'll be for another day :)
2026-04-13 17:20:57 +02:00
Benjamin Bouvier d438ee2ca0 refactor(tests): add a test helper to send with a delay, use it in timeline/queue 2026-04-13 17:20:57 +02:00
Benjamin Bouvier a3fe9eef12 refactor(tests): make use of MatrixMockServer in timeline/queue 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 60056ba205 refactor(tests): make use of MatrixMockServer in timeline/subscribe 2026-04-13 17:20:57 +02:00
Benjamin Bouvier ff62ed667c refactor(tests): make use of MatrixMockServer in timeline/pinned_event 2026-04-13 17:20:57 +02:00
Benjamin Bouvier 4168362912 refactor(tests): make use of MatrixMockServer in the timeline/profiles tests 2026-04-13 17:20:57 +02:00
ganfra 96d63b7c8d doc: update changelogs 2026-04-13 16:57:57 +02:00
ganfra 260eaeea2b feat (live location): rewrite live location shares subscription logic 2026-04-13 16:57:57 +02:00
ganfra f7442fa279 fix(live location): include BeaconInfo in required state 2026-04-13 16:57:57 +02:00
ganfra da3cf3d54b doc: update changelog 2026-04-13 16:57:06 +02:00
ganfra 872861e90e Handle beacon stop aggregation in latest event
Support BeaconStop aggregation to properly display stopped
live location sharing in the latest event timeline content.
2026-04-13 16:57:06 +02:00
Hubert Chathi 36cca4e444 add comment 2026-04-10 14:52:39 +01:00
Hubert Chathi bb6202c76b fix lint 2026-04-10 14:52:39 +01:00
Hubert Chathi 5458a5afd2 add changelog 2026-04-10 14:52:39 +01:00
Hubert Chathi 45fa860b6b add secret pushing feature to FFI 2026-04-10 14:52:39 +01:00
Hubert Chathi 4ad25e32af handle the backup key being pushed from other devices 2026-04-10 14:52:39 +01:00
Hubert Chathi 423ebf9502 push the backup key to our other devices when we create a new backup 2026-04-10 14:52:39 +01:00
Damir Jelić 7fbb8cf9e5 fix(spaces): Ensure space children ordering is transitive
This patch fixes the SpaceRoom::compare_rooms method to be transitive
(If A ≤ B and B ≤ C then A ≤ C). The transitive property of a comparison
function is required by the sorting functions we are using.

If the property doesn't hold, sorting will panic.

The previous logic violated strict weak ordering by falling back towards
room ID comparison as soon as one of the values doesn't have a
SpaceRoomChildState.

This introduced inconsistent ordering paths where:
    - A and B would be compared using the SpaceRoomChildState
    - B and C would be compared using only the room ID
    - A and C would be compared using only the room ID

Leading to the case where:
    - A < B due to the state
    - B < C due to the room ID
    - and finally A > C due to the room ID.

As a result, transitivity could be broken (A < B, B < C, but C < A),
leading to a panic during the sorting.
2026-04-10 15:49:26 +02:00
Damir Jelić e0cac4eb7a test(spaces): Check that space children ordering is total 2026-04-10 15:49:26 +02:00
Damir Jelić aa9cc67f32 refactor(spaces): Make the space children comparison function simpler 2026-04-10 15:49:26 +02:00
Damir Jelić 7d6569ff27 test(spaces): Test that the top-level space ordering is total 2026-04-10 15:49:26 +02:00
Damir Jelić 4d7b19999b refactor(space): Move the top-level space comparison logic into a separate function 2026-04-10 15:49:26 +02:00
Daniel Salinas d47306045f Add cfg around inadvertent use of sqlite in matrix-sdk-ffi crate
Several new functions implicitly rely on the sqlite config.
sqlite might not be present if you are using the indexedb store, in which case
these functions are likely irrelevant.

This change conditionalizes their compilation.
2026-04-10 14:11:36 +02:00
Philippe Bertin ba461598b2 feat(ffi): expose latest json
Signed-off-by: Philippe Bertin <pbertin@teladochealth.com>
2026-04-09 15:57:29 +02:00
Stanislav Skobelkin 0233d03d4e fix(ffi): Export Eq and Hash traits in enums used as HashMap keys
matrix_sdk_ffi::ruma::TagName and matrix_sdk_ffi::event::TimelineEventType enums
that are used as HashMap keys by matrix_sdk_ffi::ruma::Tag and
matrix_sdk_ffi::room::power_levels::RoomPowerLevels respectively should probably
export Eq and Hash traits, so that we can generate bindings for languages that
implement uniffi's record/HashMap type using a hash table (e.g.
std::unordered_map in C++)

Signed-off-by: Stanislav Skobelkin <stanislav@skobelk.in>
2026-04-09 12:12:57 +02:00
Damir Jelić 725f74cbe8 fix(ffi): Correctly return that the backup is complete if the backup key is valid 2026-04-09 12:01:04 +02:00
Benjamin Bouvier 98aceb40c2 refactor(timeline): make sure all the timeline background tasks are aborted on drop
Otherwise, we might run into leaks!
2026-04-08 16:19:09 +02:00
Damir Jelić 34a35ad32a feat(ffi): Allow passing in a SecretsBundle after logging in (#6212)
This allows people to get a secrets bundle out of band or out of a database and import it
after logging in a new client.

Mainly targeted to support the Element Classic -> Element X migration.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Benoit Marty <benoitm@matrix.org>
2026-04-08 14:07:57 +00:00
Benjamin Bouvier 99971a2347 chore(sdk): add a changelog entry for the task monitor 2026-04-08 15:14:44 +02:00
Benjamin Bouvier b5f5a9c90e refactor(sdk): rename functions to spawn background tasks 2026-04-08 15:14:44 +02:00
Benjamin Bouvier ec4e228620 feat(sdk): use the new method in the context of two background jobs 2026-04-08 15:14:44 +02:00
Benjamin Bouvier a2c254d7da feat(sdk): add a way to spawn one-off jobs 2026-04-08 15:14:44 +02:00
Kevin Boos 557c0698ba Spaces: add suggested field to SpaceRoom (#6417)
Previously there wasn't a way to determine whether a given sub-space or
room within a given space had been marked as "suggested".
This was one of the things missing in support for spaces (#227) even
though it wasn't explicitly mentioned.
   
I tested that this fix does work properly in
[Robrix](https://github.com/project-robius/robrix).

Signed-off-by: Kevin Boos kevinaboos@gmail.com
2026-04-08 13:58:56 +02:00
Bryant Mairs 99a5568dbf feat: add missing APIs to HomeserverCapabilities
Add `m.room_versions` & `m.account_moderation` that were missed in the
first implementation.

Signed-off-by: Bryant Mairs <bryant@mai.rs>
2026-04-08 13:48:20 +02:00
Jonas Platte 94d8674709 refactor(ffi): Deduplicate thumbnail conversion code 2026-04-08 07:58:36 +02:00
Jonas Platte dfc4e03d5b refactor(ffi): Replace nested match with if-let chain 2026-04-08 07:58:36 +02:00
Jonas Platte 14b8248e4b refactor(ffi): Extract closure as fn to reduce indentation 2026-04-08 07:58:36 +02:00
Jonas Platte 447c89aed4 refactor(ffi): Merge separate impl blocks 2026-04-08 07:58:36 +02:00
Jonas Platte 894f94c383 Finish Rust Edition 2024 migration 2026-04-08 07:56:03 +02:00
Johannes Marbach 1f3dea778b feat(send_queue): send redactions via the send queue (#6250)
This is a first step towards
https://github.com/matrix-org/matrix-rust-sdk/issues/4162 and adds a way
to send redactions (including their local echoes) via the send queue.

I had to introduce new variants for `SentRequestKey` and
`LocalEchoContent` because in some room versions the redacted event ID
sits at the top-level of the event rather than in `content`.

At the timeline level redactions are handled via a new boolean flag in
`AggregationKind::Redaction`. Local echoes of redactions merely set a
flag on the timeline event whereas remote echoes of redactions lead to
actual redactions as before.

The FFI bindings will be updated in a follow-up PR.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-04-07 18:02:59 +02:00
Damir Jelić b07069170f Merge pull request #6409 from mgoldenberg/remove-native-tls
Remove support for `native-tls`
2026-04-07 16:03:09 +02:00
Michael Goldenberg f685feed40 doc(sdk): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:45:29 -04:00
Michael Goldenberg 610d4ca4e5 doc(ui): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:45:27 -04:00
Michael Goldenberg 41edb08283 doc(ffi): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:34 -04:00
Michael Goldenberg 44aad14732 doc: update references to tls features
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:34 -04:00
Michael Goldenberg b1fbf31c2e feat(sdk): remove rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 2bcab72dbb ci: remove reference to matrix-sdk/rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 3bac5d250d refactor(benchmarks): remove reference to matrix-sdk/rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 4ec75b5b5c feat(ui): remove rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg c8c9a94995 feat(ffi): remove rustls-tls feature flag
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 08b80c533a feat(sdk): enforce use of rustls + remove extraneous build checks
Note that the rustls-tls feature flag is temporarily left in place
to keep other crates from breaking, but it becomes a trivial flag.
It will be removed altogether in a later commit.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 0e679904e3 feat(sdk): remove support for native-tls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg 89c50c7ecd feat(ffi): remove support for native-tls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Michael Goldenberg d2857ee20a feat(ui): remove support for native-tls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-04-07 09:44:33 -04:00
Hubert Chathi c1274cea14 Add support for pushing secrets and receiving secret pushes (#6164)
see MSC4385

Pushing secrets allow devices to send secrets to other devices without waiting for a secret request.
2026-04-07 12:26:17 +01:00
Jonas Platte 12cd1effdc refactor(ffi): Fix new clippy lints 2026-04-07 12:39:12 +02:00
Jonas Platte 3a14b73258 chore(ffi): Rerun cargo fmt 2026-04-07 12:39:12 +02:00
Jonas Platte d97f4ab8b3 chore(ffi): Upgrade to Rust edition 2024 2026-04-07 12:39:12 +02:00
Bryant Mairs 3539487c49 feat: expose call_intent for m.rtc.notification
This allows for notifications to use the intent for deciding how to
render the message (e.g. whether to call it a "call" or a "video call").

Signed-off-by: Bryant Mairs <bryant@mai.rs>
2026-04-07 10:26:26 +02:00
dependabot[bot] 526f35e3d3 chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.2 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/671740ac38dd9b0130fbe1cec585b89eea48d3de...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:18:38 +02:00
dependabot[bot] 6b4e2655cc chore(deps): bump actions/deploy-pages from 4.0.5 to 5.0.0
Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4.0.5 to 5.0.0.
- [Release notes](https://github.com/actions/deploy-pages/releases)
- [Commits](https://github.com/actions/deploy-pages/compare/d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e...cd2ce8fcbc39b97be8ca5fce6e763baed58fa128)

---
updated-dependencies:
- dependency-name: actions/deploy-pages
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-07 10:18:11 +02:00
Kévin Commaille 30de5372eb Upgrade Ruma after api::Error breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-07 10:17:42 +02:00
Kévin Commaille ba451b67c9 Upgrade Ruma after RoomAliases removal
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-07 10:17:42 +02:00
Benjamin Bouvier a65fec3175 refactor(event cache): use a const array for receipt types 2026-04-02 14:29:09 +02:00
Benjamin Bouvier 6298b5c176 feat(event cache): include read receipts that were dormant in the state store 2026-04-02 14:29:09 +02:00
Benjamin Bouvier 0283ae14d9 doc(base): clarify comments around read receipts related store methods 2026-04-02 14:29:09 +02:00
Benjamin Bouvier a1157d2c38 refactor(event cache): clarify warning when a pinned event couldn't be loaded
The previous warning could trigger with abnormal numbers, since there
could be related events applying to a pinned event, the `loaded_events`
list could be bigger than the list of `pinned_event_ids`, due to the
eager flattening. This patch makes it display the correct number, by
postponing the flattening after the warning.
2026-04-02 12:39:18 +02:00
Valere Fedronic fcce02fc11 feat(widget): Support for avatars in calls via msc4039 (#6354)
Allows to get the avatars in Element Call widget !
Using
[MSC4039](https://github.com/nordeck/matrix-spec-proposals/blob/nic/feat/widgetapi-upload-files/proposals/4039-widget-api-media.md)

Counter part of web support:
- https://github.com/element-hq/element-web/pull/32755
- https://github.com/element-hq/element-call/pull/3780

Needs a EC PR to support passing data as base64 via the widget json API
- https://github.com/element-hq/element-call/pull/3818
2026-04-02 13:35:22 +03:00
Benjamin Bouvier 7cea5be9e2 refactor(timeline): allow creating a TimelineItemContent from a sdk::TimelineEvent 2026-04-02 12:34:01 +02:00
Bertin Philippe 32e9626e0f feat(ffi): expose event types and content (#6387)
Signed-off-by: Philippe Bertin <pbertin@teladochealth.com>
2026-04-02 09:14:57 +02:00
Jonas Platte e376670af3 refactor(xtask): Fix clippy lints 2026-04-02 09:10:51 +02:00
Jonas Platte 7f08793360 chore(base): Delete outdated comments 2026-04-02 09:09:55 +02:00
Benjamin Bouvier 1fc7b34016 test: add an integration test reflecting the "bouncing" behavior of the timeline after sending an event 2026-04-01 14:59:33 +02:00
Benjamin Bouvier 37447e16e5 refactor(sdk): use the TimelineEvent::sender() helper in more places
This new helper has been added recently, and it can be reused in many
more places which were doing the same thing more verbously in many
places.
2026-04-01 14:59:13 +02:00
Benjamin Bouvier 782355b556 fix(timeline): fix timeline initialization races
For non-live timelines, the recurring pattern was the following:

- first, fill the timeline with initial events, with a first call to
some form of subscribe() method,
- then, subscribe to updates in a second time later, using a second call
to the same subscribe() method.

Unfortunately, this is wrong, and opens a window for a very small race:

- first call to subscribe() to get the initial events
- new updates are emitted in the receiver, that lead to new events
- second call to subscribe() to get the receiver, and subscribe to it

In this case, the new updates would be lost by an observer.

The patch consists in refactoring timeline initialization, such that the
initial events and receiver are created at the same time; if there are
some updates happening after the now single subscribe() call, the
updates are accumulated and will be observable by external users.

This also has the nice benefit of tidying up the number of background
task handles for non-live timelines: there should be at most one such
focus task, which is now cleanly reflected in the timeline drop handles.
2026-04-01 14:44:43 +02:00
Damir Jelić eb51c862ce chore: Add missing changelog entries for changes caused by the rand bump 2026-04-01 12:48:09 +02:00
Kévin Commaille 676c81ca80 Upgrade Ruma to latest commit
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Kévin Commaille 644c5e8a4c Upgrade Ruma to commit after PushCondition breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Kévin Commaille 34d5e6da2a Upgrade Ruma to commit after UserIdentifier breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Kévin Commaille 5936a7285f Upgrade Ruma to commit after push Action breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-04-01 10:20:21 +01:00
Mauro 62d58a21ab fix: include also stopped live location events in latest_event (#6373)
The issue is that when the most recent message was an live location event that started as live and later transitioned to a stopped state, the room list summary would ignore it entirely, even though the timeline still showed it as the latest item.

That behaviour feels incorrect. At most, the client should update the displayed description based on whether isLive is true or not.

This patch prevents the stopped live location events from being filtered out in the latest events.
2026-04-01 10:56:56 +02:00
Benjamin Bouvier 2e334fafa2 refactor(multiverse): reuse load_or_fetch_event instead of implementing it manually 2026-04-01 10:43:01 +02:00
Benjamin Bouvier e66967c46f refactor(multiverse): simplify code around search 2026-04-01 10:43:01 +02:00
Benjamin Bouvier c237659aca feat(multiverse): don't display "no results found" if the search input is empty 2026-04-01 10:43:01 +02:00
Benjamin Bouvier 8cb6f74996 chore: replace incorrect trace message in search indexing task 2026-04-01 10:43:01 +02:00
Doug 0afd7c9528 fix: Update the iOS platform version to match the deployment target. 2026-04-01 11:12:18 +03:00
Benjamin Bouvier 388ced09a6 Merge pull request #6344 from matrix-org/bnjbvr/automatic-backpagination-unread-counts
feat(event cache): automatic back-pagination for unread counts, MVP
2026-04-01 09:26:55 +02:00
Doug 4071a55cd7 chore: Use a single Swift module when building the bindings.
This fixes support for Xcode 26.4.
2026-04-01 09:20:38 +02:00
Michael Goldenberg c04a97f706 feat(common): ensure cross-process lock generation is opaque
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 5b0c6bbafc doc: update change logs
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 1fb0f7f56a refactor(indexeddb): remove old crypto store generation key-value
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg ee9a05defe refactor(sqlite): remove old crypto store generation key-value
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg c0f746f88f refactor(crypto): remove old store generation logic
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 839786f810 refactor(sdk): generalize client encryption store lock fns
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 4e4b508c38 refactor(sdk): use consistent errors for client encryption store locks
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 313e634996 refactor(sdk): clean up errors in client encryption
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg ae72fcf663 refactor(sdk): rely on dirtiness reported by crypto store's cross-process lock
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Michael Goldenberg 12e9a2a159 feat(common): expose generation in cross-process lock
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-31 17:34:11 +02:00
Damir Jelić 16c1b9b57f chore: Bump rand 2026-03-31 16:57:50 +02:00
Benjamin Bouvier 06e6dd05c3 chore(docs): publicly expose the AutomaticPagination API object for external users 2026-03-31 16:36:13 +02:00
Benjamin Bouvier 1cf84e203e chore(event cache): make clippy pass on automatic pagination 2026-03-31 15:57:36 +02:00
Benjamin Bouvier 9913cce946 Merge branch 'main' into bnjbvr/automatic-backpagination-unread-counts 2026-03-31 15:55:44 +02:00
Benjamin Bouvier 1ee88b176c refactor(event cache): create a standalone AutomaticPagination API object 2026-03-31 15:46:46 +02:00
Daniel Salinas f0193cc8ac Fix indexedb migration to clean up gaps + chunks properly 2026-03-31 15:37:36 +02:00
Jorge Martín 5d2eab119d feat: Improve getting homeserver capabilities
This extracts the `/capabilities` logic to its own `HomeserverCapabilities` component in the SDK that can be manually asked to fetch, cache locally and return these capabilities.
2026-03-31 15:25:37 +02:00
Benjamin Bouvier ca91b6a278 fix(event cache): handle read receipt after a duplicate-only sync response 2026-03-31 12:52:18 +02:00
Benjamin Bouvier 260fec8750 test(event cache): regression test for lack of unread count updates after a duplicate-only sync response 2026-03-31 12:52:18 +02:00
Benjamin Bouvier 950fcd289d refactor(ffi): only start with a reset diff if the timeline isn't empty 2026-03-31 11:17:34 +02:00
Bertin Philippe 2214aded9d feat(ffi): expose Client.sync() (#6359)
### Expose sync v2 API through FFI via `Client.sync()` and
`Client.sync_once()` #6348

Mobile clients can now sync using the traditional `/sync` v2 endpoint
through the FFI bindings, without requiring Sliding Sync (MSC4186)
support on the homeserver.

This PR introduces a new API (non-breaking change).

#### Implementation notes

I chose to expose only the **list of room IDs** (invited, joined, left,
knocked) in the `SyncResponse`, rather than forwarding the full per-room
event payloads. This keeps the FFI surface simple. Clients that need
room details can query them through the existing `Room` APIs after being
notified of changes via the listener.

Two entry points are provided:

- **`Client.sync(settings, listener)`** starts a continuous sync loop,
returning a `TaskHandle` for cancellation. The `SyncListener` callback
fires after each successful sync response.
- **`Client.sync_once(settings)`** performs a single sync call, useful
for initial sync or one-off use cases.

`SyncSettings` exposes `timeout_ms` and `full_state`.

Signed-off-by: Philippe Bertin <pbertin@teladochealth.com>
2026-03-31 10:40:54 +02:00
Doug 02c41e20ad chore: Remove uniffi Swift mocks workaround now that we're using 0.31.0. 2026-03-30 21:43:07 +02:00
dependabot[bot] 0e627b4ba2 chore(deps): bump actions/cache from 5.0.3 to 5.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/cdf6c1fa76f9f475f3d7449005a359c84ca0f306...668228422ae6a00e4ad889ee87cd7109ec5666a7)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:42:29 +02:00
dependabot[bot] 55d2e6a740 chore(deps): bump CodSpeedHQ/action from 4.11.1 to 4.12.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.11.1 to 4.12.1.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codspeedhq/action/compare/281164b0f014a4e7badd2c02cecad9b595b70537...1c8ae4843586d3ba879736b7f6b7b0c990757fab)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: 4.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:42:01 +02:00
dependabot[bot] ee2ed6033e chore(deps): bump moonrepo/setup-rust from 1.2.2 to 1.3.0
Bumps [moonrepo/setup-rust](https://github.com/moonrepo/setup-rust) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/moonrepo/setup-rust/releases)
- [Changelog](https://github.com/moonrepo/setup-rust/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moonrepo/setup-rust/compare/ede6de059f8046a5e236c94046823e2af11ca670...abb2d32350334249b178c401e5ec5836e0cd88d3)

---
updated-dependencies:
- dependency-name: moonrepo/setup-rust
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 21:41:36 +02:00
Damir Jelić fb97922a1c Revert "feat(sdk): Use stable OAuth 2.0 login scopes"
This reverts commit 14b21e2a9a.
2026-03-30 21:40:31 +02:00
dependabot[bot] 85b8c83d51 chore(deps): bump bnjbvr/cargo-machete from bf9b52599f213cb8a6d6cee568fc61a413c79975 to b81ce1560c5fbd0210cb66d88bf210329ff04266 (#6375)
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete)
from bf9b52599f213cb8a6d6cee568fc61a413c79975 to
b81ce1560c5fbd0210cb66d88bf210329ff04266.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md">bnjbvr/cargo-machete's
changelog</a>.</em></p>
<blockquote>
<h1>unreleased</h1>
<ul>
<li>Improved: add <code>renamed</code> table to track renamed crates (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/152">#152</a>
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/153">#153</a>).</li>
</ul>
<h1>0.7.0 (released on 2024-09-25)</h1>
<ul>
<li>Breaking change: Don't search in ignored files (those specified in
.ignore/.gitignore) by default. It's possible to use
<code>--no-ignore</code> to search in these directories by default (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/137">#137</a>).</li>
<li>Improved: fix false positives for multi dependencies single use
statements (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/120">#120</a>).
This improves precision at the cost of a small performance hit.</li>
<li>Improved: make usage of <code>--with-metadata</code> more accurate
(<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/122">#122</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/132">#132</a>).</li>
<li>Improved: instead of displaying <code>.</code> for the current
directory, <code>cargo-machete</code> will now display <code>this
directory</code> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/109">#109</a>).</li>
<li>Added: There's now an automated docker image build that publishes to
the <a
href="https://github.com/bnjbvr/cargo-machete/pkgs/container/cargo-machete">github
repository</a> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/121">#121</a>).</li>
<li>Added: <code>--ignore</code> flag which make cargo-machete respect
.ignore and .gitignore files when searching for files (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/95">#95</a>).</li>
</ul>
<h1>0.6.2 (released on 2024-03-24)</h1>
<ul>
<li>Added: shorter display when scanning the current directory (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/109">#109</a>).</li>
<li>Fix: adapt to latest pkgid specification, so as not to crash with
<code>--with-metadata</code> (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/106">#106</a>).</li>
</ul>
<h1>0.6.1 (released on 2024-02-21)</h1>
<ul>
<li>Chore: bump major dependencies, to fix parsing issues of Cargo.toml
files (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/101">#101</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/105">#105</a>).</li>
</ul>
<h1>0.6.0 (released on 2023-09-23)</h1>
<ul>
<li><em>Breaking</em>/improved: match against crate name
case-insensitive (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/69">#69</a>).</li>
<li>Added: Github action (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/85">#85</a>).
See README for documentation.</li>
<li>Added: support for ignored workspace dependencies (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/57">#57</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/86">#86</a>).
See README for documentation.</li>
<li>Added: <code>--version</code> switch to print the version (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/66">#66</a>).</li>
<li>Fix: avoid searching for workspace Cargo.toml longer than needed (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/84">#84</a>).</li>
<li>Chore: better documentation and reporting (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/63">#63</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/72">#72</a>,
<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/80">#80</a>).</li>
</ul>
<h1>0.5.0 (released on 2022-11-15)</h1>
<ul>
<li><em>Breaking</em>: Use <code>argh</code> for parsing. Now, paths of
directories to scan must be passed in the last
position, when running from the command line (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/51">#51</a>).</li>
<li>Fix rare false positive and speed up most common case (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/53">#53</a>).</li>
<li>Fix loading properties from workspace (<a
href="https://redirect.github.com/bnjbvr/cargo-machete/issues/54">#54</a>).</li>
</ul>
<h1>0.4.0 (released on 2022-10-16)</h1>
<ul>
<li>Added <code>--skip-target-dir</code> to not analyze
<code>target/</code> directories.</li>
<li>Added a message indicating of any unused dependencies were found or
not.</li>
<li>Support for workspace properties</li>
</ul>
<h1>0.3.1 (released on 2022-06-12)</h1>
<ul>
<li>Support empty global prefix, e.g. <code>use ::log;</code>.</li>
</ul>
<h1>0.3.0 (released on 2022-05-09)</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/bnjbvr/cargo-machete/commit/b81ce1560c5fbd0210cb66d88bf210329ff04266"><code>b81ce15</code></a>
build(deps): bump argh from 0.1.18 to 0.1.19</li>
<li><a
href="https://github.com/bnjbvr/cargo-machete/commit/5675104e9dc2192adf3acec0535353003045d586"><code>5675104</code></a>
build(deps): bump toml_edit from 0.25.4+spec-1.1.0 to
0.25.5+spec-1.1.0</li>
<li>See full diff in <a
href="https://github.com/bnjbvr/cargo-machete/compare/bf9b52599f213cb8a6d6cee568fc61a413c79975...b81ce1560c5fbd0210cb66d88bf210329ff04266">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-30 16:26:07 +00:00
Michael Goldenberg 179f38b252 refactor(sdk): use default crypto provider in rustls
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-30 16:40:51 +02:00
Damir Jelić 2c6409cb86 Merge pull request #6235 from Hywan/feat-sdk-event-cache-log-error-to-sentry
chore(sdk): Log errors from `EventLinkedChunk`
2026-03-30 14:03:39 +02:00
JoFrost 4d2f8bb82c feat(base): improve profile updates for left/banned users (#6097)
This avoids profile updates when a user decides to leave on their own (in which case their profile is kept as is), and when a user is banned (in which case their profile is removed).

Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2026-03-30 11:59:43 +02:00
Benjamin Bouvier c0b26b6f25 refactor(event cache): move the thread subscriber and search indexing tasks to the EventCacheDropHandles struct
This means that when some caller is subscribing to the `EventCache`,
even if the event cache drops in the background, the task will remain
active until the end of the subscriber.
2026-03-30 11:49:16 +02:00
Benjamin Bouvier 8f96c32d56 refactor(event cache): use abort_on_drop for the background task handle in EventCacheDropHandles
This avoids a few manual `abort()` call in a manual `Drop` impl.
2026-03-30 11:49:16 +02:00
Benjamin Bouvier bdd0162831 tests(event cache): make tests more resilient to the order of event cache updates 2026-03-30 11:40:19 +02:00
Benjamin Bouvier 752695c6ff chore: rename background request to automatic pagination request
And associated vocabulary and fields.
2026-03-30 11:28:05 +02:00
Benjamin Bouvier 552639763e chore: move the BackgroundRequest type, tests and task to its own file 2026-03-30 11:19:01 +02:00
Jorge Martín e65d4c44b4 fix(ffi): Replace libloading with jvm-getter for getting a JVM
The `libloading` approach only works on Android 12+
2026-03-30 11:17:58 +02:00
Benjamin Bouvier d151340882 chore(review): use an unbounded sender for BackgroundRequest 2026-03-30 11:12:39 +02:00
Benjamin Bouvier 27415dc64d chore(review): Use assert_matches and remove PartialEq/Eq impls for EventsOrigin 2026-03-30 11:04:41 +02:00
Benjamin Bouvier 4feeaf0cf5 chore(review): address first review comments 2026-03-30 11:01:57 +02:00
Benjamin Bouvier 87a2f8fab7 feat(multiverse): enable automatic back pagination in multiverse 2026-03-30 10:40:31 +02:00
Benjamin Bouvier 871cb2221b test(event cache): add an integration test for automatically paginating a room with missing receipts 2026-03-30 10:40:31 +02:00
Benjamin Bouvier d1d174d137 test(event cache): test the credit system of automatic pagination 2026-03-30 10:40:31 +02:00
Benjamin Bouvier bd7dca45ef feat(event cache): make some background pagination parameters configurable 2026-03-30 10:40:31 +02:00
Benjamin Bouvier 856b2be992 test(event cache): unit test automatic background pagination 2026-03-30 10:40:31 +02:00
Benjamin Bouvier 161750f6d0 refactor(event cache): add PartialEq to EventsOrigin 2026-03-30 10:40:31 +02:00
Benjamin Bouvier c4a95f9bbe feat(event cache): automatically request paginations when no receipt has been found when computing unread counts 2026-03-30 10:40:31 +02:00
Benjamin Bouvier ac5552807b refactor(event cache): have select_best_receipt return a result struct
This makes it simpler to test its expected behavior, and will make it
trivial to add a system to request automatic paginations in the
background.
2026-03-30 10:40:31 +02:00
Benjamin Bouvier ff8b27f99b feat(event cache): add an automatic background requests task 2026-03-30 10:40:31 +02:00
Benjamin Bouvier bf9ffd7d09 refactor(event cache): make EventCache::config/config_mut sync by using a sync lock
There wasn't good reason to use an async lock, as this lock is always
super short-lived, it can be sync, which avoids complications in the
subsequent commit when calling sync init code.
2026-03-30 10:40:31 +02:00
Benjamin Bouvier 9f18d76355 feat(event cache): add a feature toggle to enable automatic back-pagination 2026-03-30 10:40:30 +02:00
Andrew Ferrazzutti 83c6a7fdf4 Fix location & name of the built shared object 2026-03-30 10:31:07 +02:00
Andrew Ferrazzutti 67fbba1e87 Tweak suggested ANDROID_NDK path
- Remove redundant trailing slash
- Clarify what the version component may be
2026-03-30 10:31:07 +02:00
Andrew Ferrazzutti 9c66221830 Fix crypto-ffi build instructions in README
- Change the example config file to not make it appear as though it
  processes environment variables
- Remove the `ar` setting from the example config file, as that setting
  is deprecated and unused (see
  https://doc.rust-lang.org/cargo/reference/config.html#targettriplear)
- Replace references of `.cargo/config` to `.cargo/config.toml`, as the
  former is deprecated
- Add example of how to set the linker through an environment variable
- Add instruction to include the NDK binary tools directory to PATH,
  because builds may fail without it
  (see https://github.com/matrix-org/matrix-rust-sdk/issues/4042)
2026-03-30 10:31:07 +02:00
Philippe Bertin f00d09597c feat(ffi): expose Room.send_state_event_raw() for sending arbitrary state events
Closes #6349

Signed-off-by: Philippe Bertin <pbertin@teladochealth.com>
2026-03-30 10:27:00 +02:00
dependabot[bot] 339f4be49c chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 2e2703d664c176156a22936b9203c38cc7a273dd to bf9b52599f213cb8a6d6cee568fc61a413c79975.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/2e2703d664c176156a22936b9203c38cc7a273dd...bf9b52599f213cb8a6d6cee568fc61a413c79975)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: bf9b52599f213cb8a6d6cee568fc61a413c79975
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 10:54:13 +03:00
dependabot[bot] f5e3e7db83 chore(deps): bump Swatinem/rust-cache from 2.9.0 to 2.9.1
Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.9.0 to 2.9.1.
- [Release notes](https://github.com/swatinem/rust-cache/releases)
- [Changelog](https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md)
- [Commits](https://github.com/swatinem/rust-cache/compare/c676846f29d98ff6b0106d3608c7ffd4048af17b...c19371144df3bb44fab255c43d04cbc2ab54d1c4)

---
updated-dependencies:
- dependency-name: Swatinem/rust-cache
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 09:48:48 +02:00
Stefan Ceriu c3a0ce15cc change(project): set the IPHONEOS_DEPLOYMENT_TARGET to 16.0
This is mainly to avoid compilation errors coming out of `aws-lc-sys` on `aarch64-apple-ios`
2026-03-30 10:39:20 +03:00
Daniel Salinas c708ed18c0 No reqwest::Certificate on wasm platforms 2026-03-27 09:06:43 +01:00
Stefan Ceriu 3db4767523 change(thread_list_service): reuse the same event construction logic for both the root and the latest thread events 2026-03-25 16:46:42 +02:00
Stefan Ceriu 050fc3d845 change(latest_events): use TimelineEvent fields instead of deserialised ones for security reasons 2026-03-25 16:46:42 +02:00
Stefan Ceriu fd572d9b33 fix(ffi): manually enter the runtime on background tasks spawning paths instead of forcing async constructors 2026-03-25 16:46:42 +02:00
Stefan Ceriu 1dd413e4bc feat(ui): subscribe to room event cache for live thread updates
When ThreadListService is created it now immediately spawns a background
task (via the client's TaskMonitor) that subscribes to the room event
cache and listens for RoomEventCacheUpdate::UpdateTimelineEvents.

For every incoming event that carries an m.thread relation pointing to a
root we are already tracking, the service rebuilds a ThreadListItemEvent via the
existing build_latest_event helper and replaces latest_event on the matching
ThreadListItem and increments num_replies by 1
2026-03-25 16:46:42 +02:00
Stefan Ceriu fe45ba5cc2 feat(ui): enrich ThreadListItem with root_event, latest_event and num_replies
Introduce ThreadListItemEvent as a shared struct for both the thread
root and the latest reply, replacing the previous flat fields on
ThreadListItem

The `latest_event` is populated from the bundled thread summary returned by the
server alongside the root event, as is the number of replies.
2026-03-25 16:46:42 +02:00
Skye Elliot 46a2ba29a0 Merge pull request #6339 from matrix-org/kaylendog/history-sharing/sync-rotate
feat(base): Discard room key on full member list update
2026-03-25 13:12:02 +00:00
Skye Elliot e877466fdc docs(base): Update CHANGELOG.md
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-25 12:57:53 +00:00
Skye Elliot be47a6435b tests: Add extra sync to history sharing session merge test
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-25 12:57:53 +00:00
Skye Elliot 3bad26e39f test(sdk): Assert room key rotated under a gappy sync v3
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-25 12:57:53 +00:00
Skye Elliot 0c3392d1d2 feat(base): Discard room key on full member list update
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-25 12:57:49 +00:00
Kévin Commaille 8dcbac3b16 Upgrade Ruma after EncryptedFile breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-25 10:34:22 +01:00
Jonas Platte ec3657d707 Upgrade const_panic
I rebased my branch from 2023. Diff still applied cleanly.
2026-03-24 23:01:19 +01:00
Benjamin Bouvier 1bf831fbb7 refactor(event cache): avoid cloning the whole RoomInfo to throw it away immediately 2026-03-24 18:07:31 +01:00
Benjamin Bouvier 708ee63ffd fix(event cache): make sure the room info is as up to date as it can be before updating read receipts
We think this might be a cause for intermittent failures of the
`room_unread_count` test on CI, as the `room_info` might be outdated
after we decided to process it.

More generally, the room info is a critical resources that should be
protected in better ways, as it's inner mutable state that can be
modified by all code claling `set_room_info` in any place, but that'll
be a problem for later.
2026-03-24 18:07:31 +01:00
Michael Goldenberg f6ff8621dc chore(deps): update sentry
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-24 13:41:25 +00:00
Stefan Ceriu 96b82a4f5d chore(docs): add ThreadListService UI and FFI changelog entries. 2026-03-24 14:51:59 +02:00
Stefan Ceriu 84bdf604e5 change(ui): remove the Room::load_thread_list method in favor of the new ThreadListService 2026-03-24 14:51:59 +02:00
Stefan Ceriu 56b13806bd feat(ffi): expose the newly introduced ThreadListService 2026-03-24 14:51:59 +02:00
Stefan Ceriu b6edf826b0 feat(ui): introduce a new ThreadListService
`ThreadListService` is the FFI-facing wrapper around
[`matrix_sdk_ui::timeline::thread_list_service::ThreadListService`]. It
maintains an observable list of [`ThreadListItem`]s and exposes a
pagination state publisher, making it straightforward to build reactive UIs
on top of the thread list.
2026-03-24 14:51:59 +02:00
ganfra e335cac8f7 fix(timeline): validate beacon_info content for session matching 2026-03-24 10:34:47 +01:00
ganfra cad9c42a7a fix(timeline): check live field directly for beacon_info item creation 2026-03-24 10:34:47 +01:00
Bryant Mairs 907a23f252 feat: add Room:is_call to check for Call rooms (MSC3417)
Signed-off-by: Bryant Mairs <bryant@mai.rs>
2026-03-24 10:32:31 +01:00
Bryant Mairs 0505edc380 Upgrade Ruma
Signed-off-by: Bryant Mairs <bryant@mai.rs>
2026-03-24 10:32:31 +01:00
Richard van der Hoff 47c7a205c7 Merge pull request #6342 from matrix-org/rav/trust_requirement_docs
Add some more docs to `CollectStrategy` and `TrustRequirement`
2026-03-23 15:29:24 +00:00
Richard van der Hoff 7986a6627e Add some more docs to CollectStrategy and TrustRequirement
Some notes to help us/me remember how these relate to MSC4153.
2026-03-23 15:14:01 +00:00
Ivan Enderlin 0358552086 feat(ui): Disable smart case for the Room List fuzzy filter.
This patch updates the fuzzy filter from using _smart case_ to _ignore
case_. Most of the time, the keyboard might activate an upper case for
the first character pressed in the search bar. Some users aren't used
to smart case and will believe there is a bug. Let's disable smart case
here.
2026-03-23 14:13:52 +01:00
Jorge Martín 46462599c0 fix(sdk): Custom certificates on Android after the rustls upgrade
Add separate methods and field to temporarily apply these custom certs using the webkpi approach.
2026-03-23 14:05:22 +01:00
Jorge Martín 98a5b05c4b fix(sdk): Allow disabling SSL verification again on Android
This will skip using the custom workaround for Android and the latest rustls, but since that was to fix an issue with the verifier and the verifier is not used anymore, it should be fine
2026-03-23 14:05:22 +01:00
Benjamin Bouvier 727af9e654 chore(event cache): remove the non-decreasing hack when computing unread counts
There was a hack added that the unread count of a room shouldn't
decrease, if the room's latest active receipt hasn't changed.
Unfortunately, this heuristic doesn't hold: if an event was counted
because it was encrypted, and it turns out that after decrypting it as a
UTD, it should now be uncounted, then the read count would in fact
legitimately decrease.

Let's remove this hack, and in the meantime keep on working on automatic
backpagination, which should help fix this kind of issues.
2026-03-23 12:30:11 +01:00
dependabot[bot] 0d59b43dfd chore(deps): bump rustls-webpki from 0.103.9 to 0.103.10
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.9 to 0.103.10.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.9...v/0.103.10)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 10:46:47 +01:00
Mauro Romito d2a019a25f change: updated tests to include also the encrypted beacon case 2026-03-23 10:46:31 +01:00
Mauro Romito cc4c296f74 change: security improvement 2026-03-23 10:46:31 +01:00
Mauro Romito 61b3966cf9 docs: changelog 2026-03-23 10:46:31 +01:00
Mauro Romito b31b743435 fix: live location shield doesn't get flagged as sent in clear 2026-03-23 10:46:31 +01:00
Valere Fedronic 2a00401e79 feat(rtc): get active call consus intent (audio/video) (#6274)
<!-- description of the changes in this PR -->

Depends on: https://github.com/ruma/ruma/pull/2414

Partity with web/js-sdk.
Allows to get the consus intent for the current call using the
membership `m.call.intent`

EW use it to change the icon in the room list
<img width="298" height="117" alt="image"
src="https://github.com/user-attachments/assets/21e59f69-e099-40a6-ae27-d9246df35b64"
/>


- [x] I've documented the public API Changes in the appropriate
`CHANGELOG.md` files.
- [ ] This PR was made with the help of AI.

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:
2026-03-23 10:42:45 +01:00
Stefan Ceriu 39255c0542 chore(live_location): expose the inner beacon_infos location sharing session start timestamp. 2026-03-20 15:26:19 +02:00
Benjamin Bouvier d9ed4f61af fix(event cache): don't select an implicit threaded receipt for counting unreads
The test shows a situation where an in-thread message would be counted
as an implicit receipt, while, when we're in the `main` timeline mode,
we're counting unreads only for the main timeline. In this case, the
test would fail because the unread counts would be set to 0, since
select_best_receipt would have selected $3.

The patch is to *not* select an implicit receipt, in that mode, when
it's in a thread.
2026-03-20 13:52:38 +01:00
Benjamin Bouvier 7beda7990f fix(timeline): don't send a main-thread receipt on a thread aggregation 2026-03-20 12:33:14 +02:00
Andy Balaam ec975094e2 Throttle our retries when attempting cross-signing reset, and give up after a time limit (#6325)
Related to https://github.com/element-hq/element-x-android/pull/6420

Part of https://github.com/element-hq/element-x-android/issues/5075

I noticed when investigating a bug about resetting your identity when
using a MAS login that we poll the server checking whether the user has
given us permission with no limit on how fast we poll, and with no
ability to give up if it's not working.

This change causes us to retry only twice per second, and give up after
2 minutes. These are guesses as to the right values and I am open to
discussion.
2026-03-20 10:13:44 +00:00
Jorge Martín cbfecf520c refactor(sdk): Remove the unwraps, and add HttpError::VerifierBuilder variant 2026-03-19 17:33:20 +01:00
Jorge Martín 6151120621 fix(sdk): False positives for revoked HTTPS certs in Android
This adds back the `webpki` verifier and sets it to a custom rustls instance created only for Android, instead of using the platform verifier that results in false positives with Let's Encrypt certs (and from other CAs).

See https://github.com/matrix-org/matrix-rust-sdk/issues/6319
2026-03-19 17:33:20 +01:00
Benjamin Bouvier 84ddafbd6c chore: rename SharedPagination to SharedPaginationTask 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 57e9225152 chore: address review comments related to tests 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 36e49c2702 refactor(event cache): merge the pagination status with the shared pagination state 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 6bce60ed8f chore: address first batch of review comments 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 43005ba0c1 chore: add changelog entry for the new behavior of pagination 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 4540dd7eef fix(test): fix test expectations in test_skip_count…
For some reason, the previous test didn't take the initial skip count
value into account, and now it does. Oh well.
2026-03-19 14:32:07 +01:00
Benjamin Bouvier 91074ed3da refactor(timeline): simplify code flow in live pagination 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 3606dc1ed9 fix(test): fix a few test expectations with respect to ordering of timeline updates
My only guess for this semantic change, is that the pagination status
update and the end of the pagination now happen at different times, or
close enough that they're regrouped in the same stream update. This
doesn't fundamentally change the semantics, so we'll see if this holds
on slower machines (e.g. CI).
2026-03-19 14:32:07 +01:00
Benjamin Bouvier 7f1d49f8f5 refactor(test): make test_back_pagination_aborted run faster
- use the modern MatrixMockServer facilities
- provide a previous-batch token so as not to wait for the initial batch
token
- lower the delay for the /messages responses to 1 sec
2026-03-19 14:32:07 +01:00
Benjamin Bouvier 1afd4e9fa1 fix(test): fix test_back_pagination_aborted expectations
Even though the task which started the back-pagination is aborted, since
it's now the event cache owning it, it keeps on running in the
background.
2026-03-19 14:32:07 +01:00
Benjamin Bouvier b5153a8b23 feat(event cache): have the event cache own the back-pagination task 2026-03-19 14:32:07 +01:00
Benjamin Bouvier 9c4a47b20b refactor(event cache): allow deref state lock read/write guards to the state
This makes it possible to common out the implementations of functions
that should be available to both kinds.

Also, moves a bit of code that could easily live in the
`RoomEventCacheState` impl block, instead of being free functions.
2026-03-19 14:21:27 +01:00
Benjamin Bouvier 43e468e149 refactor(timeline): replace a bool::to_owned() call with a dereference
booleans can be copied \o/
2026-03-19 11:53:30 +01:00
Benjamin Bouvier 993e02e02c doc(event cache): explain when the missing active receipt case can happen
And simplify a double `map()` call.
2026-03-19 11:51:34 +01:00
Benjamin Bouvier cd99520a6e chore(event cache): add logs in select_best_receipt
These will make debugging much easier in the future, even though they're
quite verbose.
2026-03-19 11:51:34 +01:00
Benjamin Bouvier 69a1bd5019 fix(event cache): stop at the active receipt, if found, when computing unread counts
There was a subtle bug that a receipt would be considered active, and
then on the subsequent call to `select_best_receipt`, it could be
forgotten in favor of an older receipt. The regression test shows one
such case, where before this patch, the count would incorrectly say 3,
not 2, because the active read receipt moved backwards to the implicit
receipt.

The solution is to stop looking for a better receipt, if we run into the
latest active read receipt. Having `found` set to `None` in this case
means we hadn't found any better read receipt anyways.
2026-03-19 11:51:34 +01:00
Kévin Commaille 1d87b33b79 Upgrade Ruma
Handle a breaking change around request `AuthScheme`s.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-19 09:55:25 +01:00
Ivan Enderlin cbc7228e08 doc(sdk): Add #6316 in the `CHANGELOG.md. 2026-03-19 09:18:37 +01:00
Ivan Enderlin affce2d43c fix(sdk): Remove an unwrap on a Result<T, JoinError>.
This patch removes the `unwrap` in `spawn().await.unwrap()` to replace
it by a proper error propagation.
2026-03-19 09:18:37 +01:00
Jorge Martín 29aa3c2e08 fix(ffi): Using rustls in Android
It turns out on Android, rustls needs [a custom setup](https://github.com/rustls/rustls-platform-verifier#android) and adding the `rustls-platform-verifier-android` library that's [not available on Maven](https://github.com/rustls/rustls-platform-verifier/issues/115).

Then, from the Android clients we'd need to call some exposed JNI function so we can provide a JVM context from where Rust can take the `Application` component and access its contents to read its credentials storage. Thanks to some tricks we can use `libloading` to simulate this call from Rust itself and properly initialise the platform verifier.

Note self-signed certificates will no longer work with these changes on Android, and providing them in `ClientBuilder::add_root_certificates` will make most requests fail. This can be handled separately.
2026-03-18 17:07:55 +01:00
Skye Elliot 81bed18550 Merge pull request #6292 from matrix-org/kaylendog/history-sharing/respect-visibility
feat(sdk): Rotate session keys when a member leaves the room
2026-03-18 14:44:03 +00:00
Skye Elliot 9082cb94a9 docs(sdk): Update CHANGELOG.md 2026-03-18 14:30:53 +00:00
Skye Elliot fd806a9c11 tests: Assert room key rotated on leave event after being offline
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-18 14:25:22 +00:00
Skye Elliot 7a26db66b5 tests: Assert room key is rotated when a member leaves the room
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-18 14:25:22 +00:00
Skye Elliot 635fdd3f80 refactor(crypto): Lock GroupSessionCache::get to cfg(test)
This method is now only used by tests, so I opted to lock it
behind the test configuration to appease CI.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-18 14:25:21 +00:00
Skye Elliot ba9e876dfe docs(sdk): Outline key rotation scenario on discard handler
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-18 14:25:21 +00:00
Skye Elliot bfe24d90cd feat(sdk): Rotate session key when member leaves the room
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-18 14:25:19 +00:00
Andy Balaam 60ce062ac5 Shorten the impl of reset on IdentityResetHandle 2026-03-18 12:59:45 +00:00
Ivan Enderlin daa1120286 chore(ui): Always enable the latest event Room List sorter.
This patch removes the `enable_latest_event_sorter` flag in
`RoomList::entry_with_dynamic_adapters_with`. This sorter is now stable
enough, we can always enable it.
2026-03-18 10:47:31 +01:00
Stefan Ceriu 4ad046d4b8 change(thread_list): update public documentation 2026-03-17 19:29:28 +02:00
Stefan Ceriu 33c156f231 feat(threads): convert the types exposed from Room.list_threads to UI crates TimelineItem types
... and closer to what we do for latest events, reply details etc.
2026-03-17 19:29:28 +02:00
Stefan Ceriu 9405908eb8 fix(ffi): actually expose the ThreadRoot details over FFI 2026-03-17 19:29:28 +02:00
Mauro Romito 5742f49886 docs: changelog updated
# Conflicts:
#	crates/matrix-sdk/CHANGELOG.md
2026-03-17 18:19:44 +01:00
Mauro Romito cabaede7fc feat: added beacon_info start event to the latest_event that can be displayed 2026-03-17 18:19:44 +01:00
Benjamin Bouvier a4bb2f3ff4 chore: add changelog entry for the cloneable EventCacheError 2026-03-17 15:47:39 +01:00
Benjamin Bouvier 816530c89c feat(event cache): make EventCacheError cloneable
This makes it possible to share futures which output is a result that
has the error type set to `EventCacheError`. See also
https://github.com/matrix-org/matrix-rust-sdk/pull/6304 for usage.
2026-03-17 15:47:39 +01:00
Ivan Enderlin e72950ea15 refactor(sdk): Inline RoomEventCacheStateLockWriteGuard::load_more_events_backwards in RoomPagination.
The method
`RoomEventCacheStateLockWriteGuard::load_more_events_backwards` is used
only in one-place: in `RoomPagination::load_more_events_backwards`. This
patch inlines this method as it aims at living in `RoomPagination`, not
somewhere else.

It's purely code move. Nothing else has changed.

This patch also updates a test that was accessing
`load_more_events_backwards` directly. Now it runs it via
`RoomPagination`.
2026-03-17 13:49:00 +01:00
Ivan Enderlin 793f62049a chore(sdk): Rename *Lock to Locked*. 2026-03-17 13:44:14 +01:00
Ivan Enderlin 2652db661b chore(sdk): Add a temporary `allow(clippy). 2026-03-17 13:44:14 +01:00
Ivan Enderlin 2459348b2e doc(sdk): Add #6280 in the CHANGELOG.md. 2026-03-17 13:44:14 +01:00
Ivan Enderlin b85bb5463f refactor(sdk): In ThreadPagination, replace RoomEventCacheInner by ThreadEventCacheInner.
This patch updates `ThreadPagination` to hold a `ThreadEventCacheInner`
instead of a `RoomEventCacheInner`! It makes more sense and it
splits/isolates the types even more.

`RoomEventCache::thread_pagination` is now async and returns a
`Result<ThreadPagination>` because it needs to load its state to fetch
a `ThreadEventCache`. Later, accessing a thread wouldn't happen in
`RoomEventCache` but in `Caches`, one step at a time.
2026-03-17 13:44:14 +01:00
Ivan Enderlin 7929bcc956 chore(sdk): Add ThreadEventCacheInner::weak_room.
This patch adds the `weak_room: WeakRoom` field to
`ThreadEventCacheInner`. This is a prerequisite to have
`ThreadPagination` uses `ThreadEventCacheInner` instead of
`RoomEventCacheInner`!
2026-03-17 13:44:14 +01:00
Ivan Enderlin a91d24c53e refactor(sdk): Create ThreadEventCacheState.
This patch creates the `ThreadEventCacheState` type. It uses
`caches::lock::StateLock`, just like `RoomEventCacheState`. It allows
to have the `read()` and `write()` method to access the state, and to
reload it when necessary, see the `caches::lock::Store` implementation.

This patch thus creates `ThreadEventCacheStateLockReadGuard` and
`ThreadEventCacheStateLockWriteGuard`. The methods touching the state in
`ThreadEventCacheInner` are moved to these lock types.

They are purely code moves (plus changes to reach the correct data): no
change in the semantics.
2026-03-17 13:44:14 +01:00
Ivan Enderlin e55db521a4 refactor(sdk): Create ThreadEventCacheInner.
This patch creates `ThreadEventCacheInner` so that `ThreadEventCache`
can be shallow cloned (which will be useful for `ThreadPagination`).
That's also the first step to introduce `ThreadEventCacheState`!
2026-03-17 13:44:14 +01:00
Ivan Enderlin c71e5c14c1 chore(sdk): Move the has_new_gap definition a bit earlier.
This patch moves up the `has_new_gap` definition to clarify the code
even more.
2026-03-17 13:44:14 +01:00
dependabot[bot] 33f3a67020 chore(deps): update dtolnay/rust-toolchain requirement to 631a55b12751854ce901bb631d5902ceb48146f7
Updates the requirements on [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) to permit the latest version.
- [Release notes](https://github.com/dtolnay/rust-toolchain/releases)
- [Commits](https://github.com/dtolnay/rust-toolchain/commits/631a55b12751854ce901bb631d5902ceb48146f7)

---
updated-dependencies:
- dependency-name: dtolnay/rust-toolchain
  dependency-version: 631a55b12751854ce901bb631d5902ceb48146f7
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 13:29:15 +01:00
Stefan Ceriu 290650c6b1 change(live_location): Move LiveLocation out of TimelineItemContent and into MsgLikeKind
... so that it has access to `MsgLikeContent` reactions
2026-03-17 13:39:29 +02:00
dependabot[bot] 538afaf252 chore(deps): bump actions/checkout from 6.0.0 to 6.0.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...de0fac2e4500dabe0009e67214ff5f5447ce83dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 10:52:30 +01:00
dependabot[bot] 6a408d673f chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 8a8e0cd1046f54206f6d2e050cf028e6db9dbc75 to 2e2703d664c176156a22936b9203c38cc7a273dd.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/8a8e0cd1046f54206f6d2e050cf028e6db9dbc75...2e2703d664c176156a22936b9203c38cc7a273dd)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: 2e2703d664c176156a22936b9203c38cc7a273dd
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 10:48:41 +01:00
dependabot[bot] 8a6f822993 chore(deps): bump codecov/test-results-action from 1.1.1 to 1.2.1
Bumps [codecov/test-results-action](https://github.com/codecov/test-results-action) from 1.1.1 to 1.2.1.
- [Release notes](https://github.com/codecov/test-results-action/releases)
- [Commits](https://github.com/codecov/test-results-action/compare/v1.1.1...0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3)

---
updated-dependencies:
- dependency-name: codecov/test-results-action
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 10:43:36 +01:00
Damir Jelić aa669585b4 ci: Pin our github actions to commit hashes 2026-03-17 10:24:44 +01:00
dependabot[bot] 109db5ee58 chore(deps): bump lz4_flex from 0.11.5 to 0.11.6
Bumps [lz4_flex](https://github.com/pseitz/lz4_flex) from 0.11.5 to 0.11.6.
- [Release notes](https://github.com/pseitz/lz4_flex/releases)
- [Changelog](https://github.com/PSeitz/lz4_flex/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pseitz/lz4_flex/compare/0.11.5...0.11.6)

---
updated-dependencies:
- dependency-name: lz4_flex
  dependency-version: 0.11.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 00:28:31 +01:00
Benjamin Bouvier abc6b4ad01 doc(event cache): improve documentation around select_best_receipt 2026-03-16 17:03:00 +01:00
Benjamin Bouvier 241584366d refactor(event cache): remove ReceiptSelector and associated tests
Now that we use `select_best_receipt` and that it's properly tested, we
can entirely get rid of the `ReceiptSelector` and associated tests.
2026-03-16 17:03:00 +01:00
Benjamin Bouvier 8a611305cf test(event cache): add unit tests for select_best_receipt 2026-03-16 17:03:00 +01:00
Benjamin Bouvier 0c149155be refactor(event cache): directly use the linked chunk for computing unread counts 2026-03-16 17:03:00 +01:00
Benjamin Bouvier bc69d36d3e refactor(event cache): remove test-only compute_unread_counts_legacy and associated tests
The new integration tests for the event cache cover the same situations
that were tested by the previous tests on compute_unread_count_legacy,
so we're fine here.
2026-03-16 17:03:00 +01:00
Benjamin Bouvier a453f019d9 fix(unread counts): after a gappy sync, don't decrease unread counts
A gappy sync may cause a linked chunk to shrink, waiting for callers to
lazy-reload it again in the future. But, because the unread counts
computation rely on the in-memory linked chunk, this means that the
values computed for the unread count may be incorrect (and decrease).

Fortunately, this situation is rather easy to detect, because the latest
active read receipt doesn't change in this case, so we can first check
that, and then manually readjust the unread counts, if they've
decreased.

Future work should trigger back-pagination in those cases, so the unread
counts keeps on being precise, despite the gappy sync.
2026-03-16 16:41:31 +01:00
Benjamin Bouvier f1a50bb68f test(event cache): cache shrinking confuses the unread count computation
This test exhibits an edge case: when a room event cache is shrunk
(because of a gappy/limited sync), then the unread count might decrease,
without the latest active read receipt changing.
2026-03-16 16:41:31 +01:00
Benjamin Bouvier 9cae7ba44f doc(base): improve the doc comments around unread_notification_counts
And also move the `Room::read_receipts` method below the three `unread_`
methods, for tidiness.
2026-03-16 15:50:34 +01:00
Benjamin Bouvier f00f6103c8 fix(test): bump the wait time between room-info diffs in integration testing
`test_room_notification_count` started to intermittently fail on main,
because the computation of unread counts has moved from the sliding sync
processing to the event cache. As a result, new irrelevant RoomInfo
updates (related to the unread counts) can happen, and they might happen
quickly enough that the server reponse for sending an event happens
after 2 seconds (remember, we need to factor in the time to do the e2ee
key exchange, and so on and so forth).

Bumping the time between two RoomInfo updates should be sufficient to
avoid the intermittent failure.
2026-03-16 15:50:34 +01:00
Jorge Martín d0980b6608 refactor(ffi): Remove unnecessary match branch in ffi::Client::new
This was just instantiating a value that was never used.
2026-03-16 14:22:49 +01:00
Kévin Commaille 74c1044b7d Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-16 12:53:47 +01:00
Kévin Commaille 14b21e2a9a feat(sdk): Use stable OAuth 2.0 login scopes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-16 12:53:47 +01:00
Ivan Enderlin d6c666a88d fix(xtask): The pos field can be absent when computing log sync.
This patch fixes a bug in `xtask log sync` which can miss a `sync_once`
log when the `pos` field is absent. It happens when there is no `pos`!

Example where `pos` is absent before `timeout`. Note the double space
before `timeout`:

```
… > sync_once{conn_id="room-list"  timeout=0} > send{request_id="REQ-15" …
```

While when the `pos` is present, it's:

```
… > sync_once{conn_id="room-list" pos="0/m67590980…" timeout=30000} > send{request_id="REQ-23" …
```
2026-03-16 11:16:44 +01:00
Hugh Nimmo-Smith acda2e88e3 Add HumanQrLoginError::UnsupportedQrCodeType and HumanQrGrantLoginError::UnsupportedQrCodeType 2026-03-13 08:10:19 +01:00
Skye Elliot b6c4bca5a0 Merge pull request #6275 from matrix-org/kaylendog/history-sharing/respect-visibility
feat(sdk): Only share history if room history visibility is shared
2026-03-12 14:55:15 +00:00
Damir Jelić 3f7d53a3ce feat(ffi): Add some more getters to the QRcodeData struct 2026-03-12 15:13:38 +01:00
Benjamin Bouvier a99e79ac5d chore(sdk): add a changelog entry for the new unread count computation 2026-03-12 13:28:13 +01:00
Benjamin Bouvier a191ab45ad doc(event cache): update read receipts module comment 2026-03-12 13:28:13 +01:00
Benjamin Bouvier b5cf360111 doc(event cache): precise strategy to avoid all_events cloning 2026-03-12 13:28:13 +01:00
Benjamin Bouvier 56d5086aa4 doc: depersonalize doc comment 2026-03-12 13:28:13 +01:00
Benjamin Bouvier 6309498f20 refactor(event cache): parse as little as possible when computing read receipts 2026-03-12 13:28:13 +01:00
Benjamin Bouvier f4bee4b7a5 test(event cache): add event-cache specific tests for read receipts 2026-03-12 13:28:13 +01:00
Benjamin Bouvier 9a257a4ca3 test(unread count): check that the unread counts get updated after event decryption 2026-03-12 13:28:13 +01:00
Benjamin Bouvier cca8b0898f refactor(unread counts): use a bool to indicate thread support 2026-03-12 13:28:13 +01:00
Benjamin Bouvier a0ce0cfaf2 feat(event cache): compute read receipts in the event cache (part 2)
This is a basic implementation that works, but it should unlock
improvements already (getting the unread count updated whenever a UTD
has been resolved) and it will pave the way for future improvements
(notably with respect to performance).
2026-03-12 13:28:13 +01:00
Benjamin Bouvier 627e118ef3 refactor(sdk): move the read receipt code handling to the event cache (part 1)
This is almost only code motion in this commit.

At this point, some tests don't pass, as the support for using the read
receipt code in the event cache isn't plugged in to the event cache
itself.
2026-03-12 13:28:13 +01:00
Benjamin Bouvier c55935f92c fix(base): don't accumulate unreads when a receipt is pending 2026-03-12 13:28:13 +01:00
Benjamin Bouvier be1f525ccc refactor(base): rejigger unread count code to use iterators on all events
This will allow it to run on the room linked chunk's events immediately,
with minimal changes.
2026-03-12 13:28:13 +01:00
Ivan Enderlin a48f23881d chore(sdk): Make RoomEventCacheInner more private.
This patch makes `RoomEventCacheInner` more private, from `event_cache`
to `event_cache::caches`.

Consequently, `PinnedEventCacheState`, `RoomPagination::new` and
`ThreadPagination::new` follows the same restriction.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 95aaa6558c chore(sdk): Make RoomEventCache::new, handle_*_room_update more private.
This patch makes `RoomEventCache::new`,
`RoomEventCache::handle_joined_room_update` and
`RoomEventCache::handle_left_room_update` more private. They are no more
accessible from `event_cache` but only from `event_cache::caches`.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 375d3a4921 test(sdk): Move the test_uniq_read_marker test.
This patch moves `test_uniq_read_marker` from `event_cache` to
`event-cache::caches::room`.

This patch is a prerequisite to the next patch where the
`RoomEventCache::handle_joined_room_update` will become more private.
Moving this test is necessary.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 8b29fc8c08 chore(sdk): Move EventLocation.
This patch moves `EventLocation` from `event_cache::caches::room` to
`event_cache::caches`.

This type is used by the redecryptor, and can possibly be used by other
event caches. So let's move it in the `caches` module.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 50f97f3377 chore(sdk): EventCacheInner::thread_subscriber_receiver is behind feature = "testing".
This patch puts the `EventCache::subscribe_thread_subscriber_updates`
method behind `#[cfg(feature = "testing")]` along with the
`EventCacheInner::thread_subscriber_receiver` field.

This data is only used for tests, as the documentation tells so. Let
make it clear. Also, it reduces the size `EventCacheInner` by 16 bytes
for non-testing builds.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 0ce36d0bfb fix(sdk): EventCacheInner::all_caches_for_room returns a reference to Caches.
This patch updates `EventCacheInner::all_caches_for_rooms` by returning
an `OwnedRwLockReadGuard` instead of `Caches`. `Caches` no longer
implement `Clone`, which prevents cloning all the event caches per room
when we need only one for example.

`Caches` has two new methods: `handle_joined_room_update` and
`handle_left_room_update`. This type acts more like a dispatcher now,
which was the initial idea. That way, we don't need to dispatch manually
on all event caches: `Caches` is responsible for that.

We need to be a bit careful now since `all_caches_for_rooms` returns an
owned read-lock over `EventCacheInner::by_room`.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 9608aa5840 chore(sdk): Add Caches::all_events.
This patch adds the `Caches::all_events` to iterate over all events from
all caches.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 99c7e27fcf fix(sdk): Introduce ResetCaches.
This patch introduces the `Caches::prepare_to_reset` method, which
returns a `ResetCaches` type. This new type is responsible to lock all
the event caches related to a room, and to reset their in-memory state
with `ResetCaches::reset_all`. This patch fixes 2 bugs (see below).

This type is acquiring exclusive locks over all the event caches
managed by `Caches`. Once dropped, all the locks are released. Note that
`ResetCaches::reset_all` takes `self`, not `&self` or `&mut self`, i.e.
it consumes `ResetCaches`, ensuring the locks are _always_ released.

This patch also fixes a possible bug when acquiring the exclusive
locks could have failed, but the error wasn't stopping the execution;
the database would have been reset anyway. Now, `try_join_all` is used
before and after resetting the database (prior to this patch, it was
using `join_all` before resetting the database).

This patch also ensures that only one reset per `Caches` can
happen at a time, by making `Caches::prepare_to_reset` taking a
`&mut self`. This was partially supported by the write lock over
`EventCacheInner::by_room`, but it would have still been possible to
reset the same `Caches` concurrently.
2026-03-12 10:07:05 +01:00
Ivan Enderlin 74fc96eec2 chore(sdk): Creates the Caches type.
This patch is the first step for the `Caches` type. It creates it, along
with the `new` constructor. More changes are required, but this is a
first step.

The pattern `Caches { room }` is used everywhere where a
`RoomEventCache` was read previously, so that we are sure the type
system will complain when we will add more fields to `Caches` (like for
threads and pinned events).
2026-03-12 10:07:05 +01:00
Skye Elliot ea74fda969 test(sdk): Make room history visibility explicit
This isn't strictly necessary, but the lack of these events was
causing spurious CI timeouts when tested with the logic that
assumed a stricter history visibility than the spec required.
2026-03-11 15:50:42 +00:00
Skye Elliot 18ea1cafc7 fix(sdk): Assume history visibility is shared if no event found 2026-03-11 15:46:39 +00:00
Skye Elliot d58873f1c3 docs(sdk): Update CHANGELOG.md 2026-03-11 14:37:46 +00:00
Skye Elliot f7d818a8ba tests(sdk): Ensure shared history respects current history visibility
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-11 14:37:42 +00:00
Skye Elliot f62fbfc4ee tests(sdk): Create helper assert_utd_with_withheld code
This commit additionally flips the order of a few assertions to
meet developer expectations, i.e. errors are reported as diffs
from expected.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-11 14:33:55 +00:00
Skye Elliot 260e227f24 feat(sdk): Only share history if room history visibility is shared
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-11 14:33:55 +00:00
Stefan Ceriu 791f4bc871 feat(sentry): add final app version and platform configuration options next to the sentry dsn 2026-03-11 15:59:32 +02:00
rasmus f812afca21 doc(sdk): add CHANGELOG.md entry for add_mentions field in Event
Signed-off-by: rasmus <mail@rasmusantons.de>
2026-03-11 14:20:40 +01:00
rasmus dd1c16a873 test(sdk): add tests for reply events with and without add_mentions
Signed-off-by: rasmus <mail@rasmusantons.de>
2026-03-11 14:20:40 +01:00
rasmus 999d612a33 feat(sdk): add add_mentions toggle to Reply
Signed-off-by: rasmus <mail@rasmusantons.de>
2026-03-11 14:20:40 +01:00
Kévin Commaille f25af6be6b fix(ffi): Don't assume that all redacted state events have an empty content
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-11 10:34:41 +01:00
dependabot[bot] af8236d708 chore(deps): bump quinn-proto from 0.11.13 to 0.11.14
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.13 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.13...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-11 09:04:19 +02:00
Stefan Ceriu 0cdaf8d794 change(live_location): emit the last becon_info asset type when subscribing to live location shares instead of none 2026-03-11 09:02:37 +02:00
Stefan Ceriu 6b414b7791 change(location_sharing): make the asset type non-optional as per original MSC instructions
- https://github.com/matrix-org/matrix-spec-proposals/blob/matthew/location/proposals/3488-location.md
- `If m.asset is missing from the location's content the client should render it as m.self as that will be the most common use case. Otherwise, if it's not missing but the type is invalid or unknown the client should attempt to render it as a generic location. Clients should be able to distinguish between m.self and explicit assets for this feature to be correctly implemented as interpreting everything as m.self is unwanted.`
- this aligns the behavior with the newly introduced live location asset type handling
2026-03-11 09:02:37 +02:00
mgoldenberg 4ec9124ce1 Allow storing the same Event in multiple LinkedChunks of the same Room (#6200)
# Overview

There are scenarios in which it is sensible to have an event exist in
the same room more than once. Notably, this is true in the context of a
thread, where an event exists in the main timeline of a room, as well as
in a thread of that same room.

Support for this behavior has been implemented in the
`SQLiteEventCacheStore` in #6065; however, this was never implemented
for the `IndexeddbEventCacheStore` or the `MemoryStore`. This pull
request extends this behavior to both of those stores.

# Changes

## Integration Tests
First, `test_event_chunks_allows_same_event_in_room_and_thread` was
moved from `matrix_sdk_sqlite::event_cache_store` to
`matrix_sdk_base::event_cache::store::integration_tests`. Then, a few
additional integration tests were added to ensure that behavior is
consistent across implementations of `EventCacheStore`.

## `IndexeddbEventCacheStore`
In order to accommodate the behavioral changes specified by the
integration tests, it was necessary to modify the schema in the
IndexedDB implementation of `EventCacheStore`. Namely, the events object
store was cleared and removed and then replaced with a nearly identical
one, the only difference being the removal of a uniqueness constraint on
one of the indices.

The remaining changes mostly involved updating the behavior of top-level
`EventCacheStore` functions - e.g., filtering out events where they were
duplicated or removing positioning information where it was not
relevant.

## `MemoryStore`
The changes to `MemoryStore` mostly involved updating the behavior of
top-level `EventCacheStore` function - e.g., filtering out events where
they were duplicated or removing positioning information where it was
not relevant.

That being said, it also involved some breaking changes to
`RelationalLinkedChunk`.

1. `RelationalLinkedChunk::items` - this function returned an `Iterator`
that did not contain information about the `LinkedChunkId`, so this
information was added to the items in the `Iterator`.
2. `RelationalLinkedChunk::save_item` - this function did not update the
item in all linked chunks of the provided `Room`. It now does this, but
requires that the provided `Item` be `Clone`.

(1) could probably have been a new function, but I thought a nicer
interface was worth the breaking change. (2) could probably be prevented
by re-organizing `RelationalLinkedChunk`'s internal data structures to
remove the `Clone` requirement, but that seemed like it could turn into
a large refactoring project, so I opted for something simpler albeit
somewhat crude.

In both cases, I'm open to suggestions and would be happy to revisit if
something else is preferred.

---
Closes #6094.

- [x] I've documented the public API Changes in the appropriate
`CHANGELOG.md` files.
- [x] I've read [the `CONTRIBUTING.md`
file](https://github.com/matrix-org/matrix-rust-sdk/blob/main/CONTRIBUTING.md),
notably the sections about Pull requests, Commit message format, and AI
policy.
- [ ] This PR was made with the help of AI.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>

---------

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-10 14:55:02 +01:00
mgoldenberg b65e450813 Support TLS v1.3 on all platforms (#6053)
**Note:** _this pull request has a companion pull request in the
[`complement-crypto`](https://github.com/matrix-org/complement-crypto/pull/229)
repository, which must be merged in conjunction with this one._

_Before merging, this should be tested in conjunction with the Element X
iOS client to ensure that TLS v1.3 is working properly._ @stefanceriu
has agreed to work on this.

## Overview

The primary change in this pull request upgrades the `reqwest`
dependency to its latest version, which defaults to using `rustls` with
support for `rustls-platform-verifier` instead of `native-tls` (see
[`reqwest@0.13.0`](https://github.com/seanmonstar/reqwest/releases/tag/v0.13.0)).
The benefit here is that `rustls` supports TLS v1.3 on all platforms,
whereas [`native-tls` does
not](https://github.com/sfackler/rust-native-tls/pull/278).

Additionally, this pull request makes `rustls` the default TLS
implementation in all the crates in this repository.

This will be particularly helpful with element-hq/element-x-ios#786.

## Changes

- `reqwest` bumped to `0.13.1`
- The API for adding/replacing certificates has changed a bit, so this
required some updating in `HttpSettings::make_client`
- `oauth2-reqwest` added in favor of `oauth2/reqwest` 
    - This is required in order to be compatible with `reqwest^0.13`
- _**`oauth2-reqwest` is currently in alpha release, so it probably
makes sense to let this stabilize a bit.**_ For details, see
https://github.com/ramosbugs/oauth2-rs/issues/333#issuecomment-3906712203.
- `getrandom` bumped to `0.3.4`
    - This is required in order to be compatible with `oauth2@5.1.0`
- `proptest` bumped to `1.9.0`
    - This is required in order to be compatible with `getrandom@0.3.4` 
- Make `rustls` the default TLS implementation

## Questions

### Mirror feature flag names?

A number of feature flags have been replaced in the dependencies above.

1. _**`reqwest/rustls-tls` => `reqwest/rustls`**_ - this is simply a
name change, but is semantically identical (see
[`reqwest@0.13.0`](https://github.com/seanmonstar/reqwest/releases/tag/v0.13.0)).
2. _**`getrandom/js` => `getrandom/wasm_js`**_ - the semantics here have
changed slightly, but it seems to just make it easier to enable the
`wasm_js` backend (see
[`getrandom@0.3.4`](https://github.com/rust-random/getrandom/blob/master/CHANGELOG.md#major-change-to-wasm_js-backend)).

At any rate, I have updated references to these flags in each of the
various `Cargo.toml` files, but have not changed the names of our
exposed features to mimic those in the dependencies.

Any thoughts or preferences on whether to mirror those names? That
would, of course, result in a breaking change.

### Default to using `rustls`? Deprecate `native-tls`?

Now that the dependencies have all been bumped, we can use `rustls` on
all platforms. Should this be the new default given that `native-tls`
will very likely never support TLS v1.3 on Apple devices? And should
`native-tls` be deprecated as a result?

**UPDATE:** _The consensus here seems to be that we should default to
using `rustls`, but that `native-tls` should still be available._

---
Fixes #5800.


- [ ] Public API changes documented in changelogs (optional)

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>

---------

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-03-10 13:38:18 +01:00
dependabot[bot] cee74d4965 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 6456c7b34a8c1245e0c186ac50c56666ccbd44f1 to 8a8e0cd1046f54206f6d2e050cf028e6db9dbc75.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/6456c7b34a8c1245e0c186ac50c56666ccbd44f1...8a8e0cd1046f54206f6d2e050cf028e6db9dbc75)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: 8a8e0cd1046f54206f6d2e050cf028e6db9dbc75
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 10:41:01 +01:00
dependabot[bot] a845bdf26a chore(deps): bump CodSpeedHQ/action from 4.11.0 to 4.11.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.11.0 to 4.11.1.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codspeedhq/action/compare/2ac572851726409c88c02a307f1ea2632a9ea59b...281164b0f014a4e7badd2c02cecad9b595b70537)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: 4.11.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 11:25:57 +02:00
dependabot[bot] 40ff661358 chore(deps): bump tj-actions/changed-files from 47.0.4 to 47.0.5
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.4 to 47.0.5.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v47.0.4...v47.0.5)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-09 17:29:19 +01:00
Andy Balaam 9de2d70bad Provide recover_and_fix_backup method to fix backup if needed (#6252)
Part of https://github.com/element-hq/element-meta/issues/3059
2026-03-09 13:32:04 +00:00
Benjamin Bouvier 6d141c07bc refactor(test): use the event factory in the notable reason read receipt update test
Turns out, the `m.read` event was invalid, because it should've been
part of the ephemeral events in the sync response (and not part of the
room's timeline response), so the event was dismissed. The test already
passed while behaving this way, so let's make it reflect what it did
indeed.
2026-03-09 14:16:52 +01:00
Benjamin Bouvier 608091fbe7 fix(test): fix test_read_receipt_can_trigger_a_notable_update_reason expectations
The test was aborted just a bit too early, in that if you introduce
arbitrary sleep statements, it would fail because the stream of notable
reasons updates wouldn't be empty, and include two `LATEST_EVENT`
updates instead.

It's not clear why we get the second update, but this isn't critical to
fix at the moment, so I'll leave this as an exercise to the reader.
2026-03-09 12:45:40 +01:00
Kévin Commaille 5e1c917459 refactor(sdk): Take a UrlOrQuery in MatrixAuth::login_with_sso_callback
To make it more convenient to use with `LocalServerBuilder` /
`LocalServerRedirectHandle`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 11:49:51 +01:00
Kévin Commaille 3e5e6efb31 refactor(sdk): Move UrlOrQuery into utils module
This will allow to reuse it outside of the `oauth` module.

It can now also be converted from a `QueryString`, for improved
compatibility with `LocalServerRedirectHandle`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 11:49:51 +01:00
Ivan Enderlin 5902a857d9 fix(xtask): log overview sanitizes HTML.
The log message can contain HTML data. It happens notably when the
homeserver is broken, and an HTML document is returned in some errors.
We don't want to parse the fields in this case, because HTML breaks
everything.
2026-03-09 11:26:44 +01:00
Kévin Commaille 168fd7232e refactor(sdk): Remove TODOs by using new Ruma API
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 10:44:56 +01:00
Kévin Commaille e17ca1071c Upgrade Ruma after Typing breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:18:19 +00:00
Kévin Commaille 99ac38eb20 Upgrade Ruma after SecretEncryptedData breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:18:19 +00:00
Kévin Commaille 698ffba88e Upgrade Ruma after report_content breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:18:19 +00:00
Kévin Commaille 23a312ed68 Upgrade ruma after ErrorKind::Forbidden breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:18:19 +00:00
Kévin Commaille 1f69fcb80b Upgrade Ruma after RequestAction breaking changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:18:19 +00:00
Kévin Commaille deabb8b6e7 refactor(ui): Construct PossiblyRedactedPinnedEventsEventContent from PinnedEventsEventContent
It is simpler than going through deserialization.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:01:39 +01:00
Kévin Commaille d8cbb1b77b refactor(ui): Call membership_change() on the event rather than providing the parts manually
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-09 09:01:39 +01:00
Kévin Commaille 7c16d673fb benchmarks: Use realistic Matrix IDs in timeline
We rely on the EventFactory to generate a hash of the events for the
event IDs, and we use the appropriate length for room IDs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Kévin Commaille e9fbbb52d5 benchmarks: Use realistic Matrix IDs in store_bench
Use the appropriate length for room IDs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Kévin Commaille c05df39132 benchmarks: Use realistic Matrix IDs in room_list
We rely on the EventFactory to generate a hash of the events for the
event IDs, and we use the appropriate length for room IDs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Kévin Commaille d572d201c9 benchmarks: Use realistic Matrix IDs in room_bench
We generate an arbitrary hash for the event IDs, and we use the
appropriate length for room IDs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Kévin Commaille 39aa777b9b benchmarks: Use realistic Matrix IDs in linked_chunk
We rely on the EventFactory to generate a hash of the events for the
event IDs, and we use the appropriate length for room IDs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Kévin Commaille f0ae0e53f9 benchmarks: Use realistic Matrix IDs in event_cache
We rely on the EventFactory to generate a hash of the events for the
event IDs, and we use the appropriate length for room IDs.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Kévin Commaille 7ec331c842 testing: Create realistic modern event IDs in EventFactory
As a fallback when the ID is not provided when constructing the event.
It allows to work with data that looks like what we would get in
production, which is important for benchmarks.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-06 15:00:45 +01:00
Skye Elliot 8472b5504e Merge pull request #6215 from matrix-org/kaylendog/history-sharing/restart-import
feat: Try import stored key bundles on client start
2026-03-05 16:23:36 +00:00
Skye Elliot 2ca717b4af docs: Update CHANGELOGs
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:03 +00:00
Skye Elliot c864fac823 test(sdk): De-duplicate shared history integration tests
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:03 +00:00
Skye Elliot c62da4a026 tests(sdk): Add integration test for bundle import crash recovery
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:03 +00:00
Skye Elliot 471e045ea3 feat(sdk): Remove pending key bundle on HTTP failure
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:02 +00:00
Skye Elliot 29ca03bb81 feat(sdk): Clear room pending key bundle for expired details
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:02 +00:00
Skye Elliot 29e3b7766d feat(sdk): Separate startup bundle import to different task
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:02 +00:00
Skye Elliot 2dd43fc9de feat(sdk): Add proper error logging and comments
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:02 +00:00
Skye Elliot 7fbc3e78e9 feat(sdk): Try import stored room key bundles on startup
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:02 +00:00
Skye Elliot ac6ccd3384 refactor(sdk): Move should_accept_bundle into shared_room_history
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-05 15:55:02 +00:00
Benjamin Bouvier f49784a15e refactor(crypto): use an async closure in Store::with_transaction
This allows passing the `Transaction` by mutable reference, instead of
passing it by ownership and requiring the callback to pass it back,
which is slightly better in terms of ergonomics. This was hard to
achieve without async closures, but now that we have them, this is
trivial.
2026-03-05 15:51:34 +01:00
Benjamin Bouvier b0aadd1574 refactor(test): use assert_let_timeout more often
Many of our tests make use of `assert_let` for checking that some value
comes out of a stream, while they could use `assert_let_timeout`, which
provides better ergonomics when the expected value doesn't arrive
immediately, by failing quickly.

This converts a few instances, making those tests easier to debug in the
future, would they fail again.
2026-03-05 15:36:41 +01:00
Stefan Ceriu 37ae2af67e fix(timeline): handle out of order beacon_info stop events/aggregations 2026-03-05 15:55:31 +02:00
Stefan Ceriu 4d64f3885a chore(timeline): add location sharing stop aggregation without timeline item test 2026-03-05 15:55:31 +02:00
Stefan Ceriu 436c598da5 chore(timeline): handle live location sharing stops similar to aggregations 2026-03-05 15:55:31 +02:00
Stefan Ceriu 034667cf3f chore(timeline): expose live location sharing asset type and cleanup public methods. 2026-03-05 15:55:31 +02:00
Stefan Ceriu 5f867ee982 chore(timeline): add live location changelogs 2026-03-05 15:55:31 +02:00
Stefan Ceriu 8521b7b65b chore(ffi): expose MSC3489 timeline types 2026-03-05 15:55:31 +02:00
Stefan Ceriu eca633b1cf chore(multiverse): handle TimelineItemContent::LiveLocation 2026-03-05 15:55:31 +02:00
Stefan Ceriu 6c769d1d33 chore(ui): add timeline live location tests 2026-03-05 15:55:31 +02:00
Stefan Ceriu 862a0e6f57 feat(ui_timeline): handle org.matrix.msc3672.beacon_info and related org.matrix.msc3672.beacon events and aggregate them onto the timeline's LiveLocation
# Conflicts:
#	crates/matrix-sdk-ui/src/timeline/event_handler.rs
2026-03-05 15:55:31 +02:00
Stefan Ceriu 2083e20592 chore(ui_timeline): explicitly filter out org.matrix.msc3672.beacon timeline events 2026-03-05 15:55:31 +02:00
Stefan Ceriu e61e86c3b4 feat(ui): add UI crate timeline types for handling MSC3489 live location sharing
# Conflicts:
#	crates/matrix-sdk-ui/src/timeline/event_item/mod.rs
#	crates/matrix-sdk-ui/src/timeline/mod.rs
2026-03-05 15:55:31 +02:00
Stefan Ceriu 17a9ab41e4 chore(tests): add event factory method for building MSC3672 beacon_info state events 2026-03-05 15:55:31 +02:00
Benjamin Bouvier 007eb15bce refactor(test): use with_versions/with_feature in a few tests and remove ok_custom 2026-03-05 14:36:42 +01:00
Benjamin Bouvier d37d65614e refactor(test): remove MockEndpoint<VersionsEndpoint>::ok_with_unstable_features
As it's now unused because of the previous commits.
2026-03-05 14:36:42 +01:00
Benjamin Bouvier 38afc0b1cd refactor(test): spell out use of the e2e_cross_signing feature in tests 2026-03-05 14:36:42 +01:00
Benjamin Bouvier 00bfffed99 refactor(test): don't use unstable features mocking when it's not needed 2026-03-05 14:36:42 +01:00
Benjamin Bouvier 964f6c8638 refactor(test): use the versions mock builder pattern in tests 2026-03-05 14:36:42 +01:00
Benjamin Bouvier df7823f1cf refactor(test): introduce a builder pattern for the VersionsEndpoint versions and features 2026-03-05 14:36:42 +01:00
Benjamin Bouvier 3457b5fa79 refactor(test): rename MockEndpoint<VersionsEndpoint>::versions to commonly_supported_versions 2026-03-05 14:36:42 +01:00
Benjamin Bouvier 21e8138805 chore: depersonalize some TODO code comments
Removed a few TODOs that were not applicable anymore, because they were
either very low value (in timeline test code) or already done (in event
cache, with respect to the cross-process locking).

Also removed my nick from some TODOs and comments, as code comments
aren't the best way to store assignees for issues.
2026-03-05 13:35:23 +01:00
Benjamin Bouvier 20d1087658 chore: remove spurious semicolon after match statement 2026-03-04 16:47:25 +01:00
Benjamin Bouvier 1edbad0bd8 refactor(client): monitor the thread subscriptions catchup task 2026-03-04 16:47:25 +01:00
Benjamin Bouvier 1825cd5816 chore: add changelog entry for the previous commit 2026-03-04 16:47:25 +01:00
Benjamin Bouvier 97e2b1c1b2 refactor(client): also consider server support when checking for thread subscriptions enablement 2026-03-04 16:47:25 +01:00
Kévin Commaille 02d0298b66 test(sdk): Add thorough tests about receiving stripped state events
Including stripped state events that fail to deserialize.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 12:23:26 +00:00
Kévin Commaille 6fd4988849 docs(base): Clarify comment for m.room.encryption
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 12:23:26 +00:00
Kévin Commaille ed319eed64 fix(base): Handle stripped state events that fail to deserialize
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 12:23:26 +00:00
Kévin Commaille 359f50f368 refactor(base): Make RoomInfo::handle_state_event generic over the enum type
This will allow to use the same method for stripped state.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 12:23:26 +00:00
Kévin Commaille c6bf11e836 Upgrade Ruma after Any(Sync)StateEvent breaking change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 12:23:26 +00:00
Benjamin Bouvier 3aec150697 doc(event cache): clarify comment about Gap::token 2026-03-04 12:13:22 +01:00
Benjamin Bouvier ddd07443f1 chore: add entry in the changelog for the renaming of the Gap::prev_token field 2026-03-04 12:13:22 +01:00
Benjamin Bouvier 8dbb6e5c1d refactor(event cache): rename Gap::prev_token to token
As it is now used for both a previous batch token (backwards pagination)
or a next batch token (forwards pagination).
2026-03-04 12:13:22 +01:00
Benjamin Bouvier cdd40d0308 chore: add changelog entry for the renaming of BackPaginationError 2026-03-04 12:06:47 +01:00
Benjamin Bouvier e9f398472e refactor(event cache): rename BackpaginationError to PaginationError 2026-03-04 12:06:47 +01:00
Benjamin Bouvier 88eb4a0da9 Merge pull request #6185 from matrix-org/bnjbvr/permalink-linked-chunk
feat: introduce an event-focused linked chunk for event-focused timelines
2026-03-04 11:39:01 +01:00
Kévin Commaille 77dddf2540 Upgrade Ruma after breaking changes of ErrorKind
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 10:30:25 +00:00
Kévin Commaille 2cf8ce2a7d Upgrade Ruma after breaking change of InvitationRecipient
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 10:30:25 +00:00
Kévin Commaille 12b1102ca9 Upgrade Ruma after breaking change about Reply relations
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 10:30:25 +00:00
Kévin Commaille 81286ad1e7 Upgrade Ruma after breaking change about EventId constructors
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-04 10:30:25 +00:00
Benjamin Bouvier 280eae0b71 refactor(event cache): extract get_event_focused_cache as a separate helper function 2026-03-04 11:15:51 +01:00
Benjamin Bouvier bc88e96e62 chore: adjust some doc comments 2026-03-04 10:52:06 +01:00
Benjamin Bouvier 37708d509c chore: add a missing feature annotation 2026-03-03 16:00:09 +01:00
Ivan Enderlin 619ded3147 chore(sdk): Log errors from EventLinkedChunk.
This patch logs errors from `EventLinkedChunk` and send them to Sentry
(if Sentry is enabled).

The trick is to use [`#[instrument(err)]`][0] for logging errors.
Quoting the documentation:

> If the function returns a `Result<T, E>` and `E` implements
> `std::fmt::Display`, adding `err` or `err(Display)` will emit error
> events when the function returns `Err`:
>
> ```rust
> #[instrument(err)]
> fn my_function(arg: usize) -> Result<(), std::io::Error> {
>     Ok(())
> }
> ```
>
> The level of the error value event defaults to `ERROR`.

It sounds exactly what we need.

[0]: https://docs.rs/tracing-attributes/0.1.31/tracing_attributes/attr.instrument.html#examples-2
2026-03-03 15:55:00 +01:00
Benjamin Bouvier 30de09aef6 refactor(event cache): rename finish_forward_pagination to push_forwards_pagination_events 2026-03-03 15:50:53 +01:00
Benjamin Bouvier 0b6d00cfad Merge branch 'main' into bnjbvr/permalink-linked-chunk-backup 2026-03-03 15:49:13 +01:00
Kévin Commaille 029148ef6e refactor(sqlite): Get rid of the DATABASE_VERSION constants
They are error prone because they need to be bumped for every migration
otherwise the new migration will not happen because we exit early.

So instead we get rid of the early returns and log each individual
upgrade separately. It makes more noise when creating a new database,
but since it is logged at the DEBUG level it is not much of a problem.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-03-03 15:26:42 +01:00
Benjamin Bouvier 8d38f6109e fix(event cache): avoid racy initialization of event-focused linked chunk 2026-03-03 15:20:04 +01:00
Benjamin Bouvier 75c5528942 refactor(event cache): add last_chunk_as_gap to EventLinkedChunk 2026-03-03 15:20:04 +01:00
Benjamin Bouvier dd5cb220a0 refactor(event cache): add first_chunk_as_gap() in EventLinkedChunk too 2026-03-03 15:09:47 +01:00
Benjamin Bouvier 4b23378d29 refactor(event cache): extract pushing a gap as a standalone function 2026-03-03 15:07:04 +01:00
Benjamin Bouvier 66c97dc02f chore: address review comments 2026-03-03 15:02:38 +01:00
Richard van der Hoff a2f77c79bc Merge pull request #6233 from matrix-org/rav/history_sharing/get_all_rooms_pending_key_bundle
Add `CryptoStore::get_all_rooms_pending_key_bundle`
2026-03-03 12:56:10 +00:00
Benjamin Bouvier 2f791d19e3 chore: add changelog entry for the recycling of internal timeline ids 2026-03-03 13:51:57 +01:00
Benjamin Bouvier 066dd4aa21 test(timeline): add test for the previous feature 2026-03-03 13:51:57 +01:00
Benjamin Bouvier a4c3a4eb87 feat(timeline): recycle internal timeline ids upon deduplication
When the event cache decides to deduplicate items, it will remove the
duplicate items, then push them back at a position further down the
line.

This can lead to spurious re-creation of items, which may show up, in
embeddings, as different items as they don't share the same internal id.

This patch makes it so that if the same transaction includes a move of
an event (i.e. it's removed then reinserted elsewhere), then the
internal ID will be reused in this case.
2026-03-03 13:51:57 +01:00
Richard van der Hoff c23dd1ec0a update changelogs 2026-03-03 12:37:15 +00:00
Richard van der Hoff 80ac2b8c38 Add integration test for get_all_rooms_pending_key_bundle 2026-03-03 12:23:57 +00:00
dependabot[bot] 617c646a52 chore(deps): bump actions/upload-artifact from 6 to 7
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-03 10:06:34 +01:00
dependabot[bot] 29b506f301 chore(deps): bump crate-ci/typos from 1.43.5 to 1.44.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.5 to 1.44.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.43.5...v1.44.0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 17:28:52 +01:00
dependabot[bot] cf84f2ff18 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 3026399e8ccbe119a9624e9376afc8c5f21fb60f to 6456c7b34a8c1245e0c186ac50c56666ccbd44f1.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/3026399e8ccbe119a9624e9376afc8c5f21fb60f...6456c7b34a8c1245e0c186ac50c56666ccbd44f1)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: 6456c7b34a8c1245e0c186ac50c56666ccbd44f1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 16:28:27 +00:00
Johannes Marbach 949bdb5bb3 fix(latest_event): handle race between send queue update and remote echo (#6220)
There seems to be a race condition where the
`RoomSendQueueUpdate::SentEvent` update arrives after the remote echo
has already been processed which leads to the latest event value getting
stuck at `LocalHasBeenSent`. This fixes the issue by trying to retrieve
the event from the cache first.

- [x] I've documented the public API Changes in the appropriate
`CHANGELOG.md` files.
- [x] I've read [the `CONTRIBUTING.md`
file](https://github.com/matrix-org/matrix-rust-sdk/blob/main/CONTRIBUTING.md),
notably the sections about Pull requests, Commit message format, and AI
policy.
- [ ] This PR was made with the help of AI.

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-03-02 15:20:17 +00:00
Valere Fedronic cc8d6d8482 bindings: Support audio/video intent in rtc notification event content (#6207)
<!-- description of the changes in this PR -->

Updates ruma from
https://github.com/ruma/ruma/commit/289bee87974bd3c2ad14a6c15801c80b683b67dc
to
https://github.com/ruma/ruma/commit/89dab44660889ab02e5921adbf6b79a3262deb99
see
https://github.com/matrix-org/matrix-rust-sdk/pull/6207/commits/4250d657564039d21f63f2fbb954b0e2eb17d9eb

Depends on this ruma PR github.com/ruma/ruma/pull/2383

Support for reading the intent from the notification event
(m.call.intent in notification event as per
https://github.com/matrix-org/matrix-spec-proposals/pull/4075)

Parity with js-sdk implementation

https://github.com/matrix-org/matrix-js-sdk/blob/bd6547c0814e11b41e79de3cc9d0d4ecf7648272/src/matrixrtc/types.ts#L150

- [ ] I've documented the public API Changes in the appropriate
`CHANGELOG.md` files.
- [ ] I've read [the `CONTRIBUTING.md`
file](https://github.com/matrix-org/matrix-rust-sdk/blob/main/CONTRIBUTING.md),
notably the sections about Pull requests, Commit message format, and AI
policy.
- [ ] This PR was made with the help of AI.

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:
2026-03-02 15:31:29 +01:00
Johannes Marbach 779d6a0925 fix(threads): include secondary relations when re-initializing a threaded timeline after a lag (#6209)
This is equivalent to what happens in `init_focus` when the timeline is
initialized from scratch.

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-03-02 15:07:24 +01:00
Benjamin Bouvier 1fdebd7d56 chore: remove the bullet point about reading the CONTRIBUTING.md file in the PR template
The PR template includes an item for a warning that is automatically
shown once (and once and for all) by the Github UI. In the previous
review of the changes to the pull request template, Ivan agreed that
this was redundant, so it seems there's no point in keeping it in the
pull request template itself.
2026-03-02 11:47:52 +01:00
Damir Jelić 39c0ee1f9b ci: Enable the MSRV check for all of our crates 2026-02-27 17:40:08 +01:00
Damir Jelić 1542a5b79e chore: Fix some new clippy warnings 2026-02-27 17:40:08 +01:00
Damir Jelić ae53b62762 chore: Bump the MSRV 2026-02-27 17:40:08 +01:00
Damir Jelić 4576013878 fix(ffi): Enable rustls by default
It's important to have one of those enabled so a simple cargo check on
the crate works.
2026-02-27 17:40:08 +01:00
Damir Jelić 73028a834e chore: Define the MSRV in all of our private crates 2026-02-27 17:40:08 +01:00
Damir Jelić 8c4f5c60b7 fix(ffi-macros): Fix compilation in case we only build this single crate 2026-02-27 17:40:08 +01:00
Ivan Enderlin aaeab050da chore(sdk): Extract EventCache::auto_shrink_linked_chunk_task to tasks.rs. 2026-02-27 16:34:28 +01:00
Ivan Enderlin 6b120505b8 chore(sdk): Extract EventCache::ignore_user_list_update_task to tasks.rs. 2026-02-27 16:34:28 +01:00
Ivan Enderlin a791243202 chore(sdk): Extract EventCache::listen_task to tasks.rs.
This patch also renames `listen_task` to `room_updates_task`.
2026-02-27 16:34:28 +01:00
Ivan Enderlin c5b7dbd0c0 chore(sdk): Extract EventCache::search_indexing_task into tasks.rs. 2026-02-27 16:34:28 +01:00
Ivan Enderlin 16e09dfefa chore(sdk): Extract EventCache::thread_subscriber_task into tasks.rs. 2026-02-27 16:34:28 +01:00
Kévin Commaille 2d13a682a2 refactor(sdk): Remove methods on OAuth API for account management URL
Instead encourage users to use the ones available on
`AuthorizationServerMetadata` because they support both the stable and
unstable actions.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-27 15:17:03 +01:00
Kévin Commaille c50bab4847 Upgrade Ruma
Brings in a few bug fixes

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-27 15:17:03 +01:00
Ivan Enderlin 768ded4b90 doc(sdk): Fix intra-links. 2026-02-27 12:49:31 +01:00
Ivan Enderlin 19ffa2fe0e chore(sdk): Inline RoomEventCacheStateLockWriteGuard::handle_backpagination.
The method `RoomEventCacheStateLockWriteGuard::handle_backpagination`
is used in a single place. This patch inlines it in…
`RoomEventCachePagination`, where it's supposed to be
declared! More precisely, inside the `PaginatedCache`
implementation for the `RoomEventCachePagination`, in the
`conclude_backwards_pagination_from_network`. The `state` module is
lighter with this change, and the code lives in the correct place.

This patch also renames `EventLinkedChunk::finish_back_pagination` to
`push_backwards_pagination_events`. The naming follows other names, like
`push_live_events`. Moreover, it removes the entire concept of “this
is part of a flow of methods”, it's just a single standalone method. On
this `EventLinkedChunk` alone, it is absolutely stateless.

This patch is purely code move, nothing changes.
2026-02-27 12:49:31 +01:00
Ivan Enderlin 3a57b547da chore(sdk): Add the RoomEventCacheUpdateSender type.
This patch creates the new `RoomEventCacheUpdateSender` type to group
both the room update sender, and the room generic update sender. It
simplifies a couple of constructor and makes the code more robust by
isolating this logic in a single type instead of two types.
2026-02-27 12:49:31 +01:00
Ivan Enderlin c3e238d2d6 chore(sdk): Make more fields of RoomEventCacheInner private. 2026-02-27 12:49:31 +01:00
Ivan Enderlin c257525b5a chore(sdk): Make RoomEventCacheInner private. 2026-02-27 12:49:31 +01:00
Ivan Enderlin 909dbd7b78 chore(sdk): Extract RoomEventCacheUpdate into caches/room/update.rs. 2026-02-27 12:49:31 +01:00
Ivan Enderlin 6c9892ee84 chore(sdk): Extract RoomEvetCacheLinkedChunkUpdate into caches/room/updates.rs. 2026-02-27 12:49:31 +01:00
Ivan Enderlin fe4381d38c chore(sdk): Extract RoomEventCacheGenericUpdate into caches/room/updates.rs. 2026-02-27 12:49:31 +01:00
Ivan Enderlin ea41e03ca3 chore(sdk): Add RoomEventCache::send_updates.
Firstly, the idea is to avoid accessing `RoomEventCache::inner`. Second,
by having a `send_updates` method, we increase the chances to forget
about one update (like `RoomEventcacheGenericUpdate` as it was the case
in the past).
2026-02-27 12:49:31 +01:00
Ivan Enderlin 9fb54074ee chore(sdk): Add RoomEventCache::state.
This patch adds `RoomEventCache::state` to replace all the `inner.state`
accesses. The idea is to make `inner` private.
2026-02-27 12:49:31 +01:00
Ivan Enderlin 597f66f71a chore(sdk): Use RoomEventCache::room_id().
This patch avoids calling `RoomEventCache::inner`.
2026-02-27 12:49:31 +01:00
Ivan Enderlin 44c71eea1b chore(sdk): Restrict the visiblity for handle_(joined|left)_room_update. 2026-02-27 12:49:31 +01:00
Ivan Enderlin d291be4235 chore(sdk): Move handle_joined_room_update from RoomEventCacheInner to RoomEventCache.
The idea of this patch is to make `RoomEventCache::inner` private. This
is required to achieve that goal.
2026-02-27 12:49:31 +01:00
Ivan Enderlin 1342daf381 chore(sdk): Move handle_left_room_update from RoomEventCacheInner to RoomEventCache.
The idea of this patch is to make `RoomEventCache::inner` private. This
is required to achieve that goal.
2026-02-27 12:49:31 +01:00
Ivan Enderlin 07f9e998e9 doc(sdk): Add missing, or fix existing documentation. 2026-02-27 12:49:31 +01:00
Ivan Enderlin a9f946d365 chore(sdk): Fix the visibility of several symbols.
This patch simplifies a lot of `pub(in super::…)` to just `pub`. The
visibility is now defined by the type itself, onto which the methods
are implemented.
2026-02-27 12:49:31 +01:00
Ivan Enderlin 248b65d2d5 chore(sdk): Extract RoomEventCacheState into caches/room/state.rs. 2026-02-27 12:49:31 +01:00
Ivan Enderlin 500a637bc4 chore(sdk): Extract RoomEventCacheSubscriber to its own subscriber module.
Purely code move, except the addition of `RoomEventCacheSubscriber::new`
to keep data isolated.
2026-02-27 12:49:31 +01:00
Hugh Nimmo-Smith 7da0d903e5 refactor: remove QRCodeGrantLoginError::UnableToCreateDevice and HumanQrGrantLoginError::UnableToCreateDevice 2026-02-27 11:40:27 +01:00
Hugh Nimmo-Smith 3cf4faec83 feat(ffi): additional HumanQrGrantLoginError enum values 2026-02-27 11:40:27 +01:00
Hugh Nimmo-Smith 2cfd53e64d feat(qr-login): additional QRCodeGrantLoginError enum values 2026-02-27 11:40:27 +01:00
Ivan Enderlin bbdfe7b38f chore(sdk): Move RoomEventCache in caches/room/mod.rs. 2026-02-26 11:39:40 +01:00
Ivan Enderlin b4977bbe5d chore(sdk): Move EventLinkedChunk in caches/event_linked_chunk.rs. 2026-02-26 11:39:40 +01:00
Ivan Enderlin 4bc4263e1c chore(sdk): Move ThreadEventCache in caches/thread/mod.rs. 2026-02-26 11:39:40 +01:00
Ivan Enderlin ef734e0876 chore(sdk): Move PinnedEvents in caches/pinned_events/mod.rs. 2026-02-26 11:39:40 +01:00
Damir Jelić a2881d5aca ci: Update our benchmarks workflow
This switches to OICD for authentication as recommended by the docs:
    https://codspeed.io/docs/integrations/ci/github-actions/configuration#oidc-recommended

Also switch to simulation mode as the instrumentation mode got renamed to
it.
2026-02-26 11:03:27 +01:00
Skye Elliot fadfd98bee feat(crypto): Add get_all_rooms_pending_key_bundle to store trait
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-02-25 16:02:41 +00:00
Skye Elliot 640fa4854f feat(sqlite): Add query_many helper trait method
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-02-25 16:02:35 +00:00
Richard van der Hoff a90cf28d3c Merge pull request #6199 from matrix-org/rav/history_sharing/invite_details_in_crypto_store
Move `invite_acceptance_details` out of `RoomInfo` and into crypto store
2026-02-25 15:16:51 +00:00
Ivan Enderlin aa11e9f062 fix(xtask): Fix a broken gap in the fields grid of overview. 2026-02-25 16:03:57 +01:00
Ivan Enderlin 4d9268d104 fix(xtask): Fix an infinite loop when parsing the fields of a span.
The error was coming from the subslice from `message` instead of
`fields`.

This patch also fixes an HTML error (`</li>` right after `<ul>`).
2026-02-25 16:03:57 +01:00
Benoit Marty b7b96d49c5 fix(ui): Check if an Event is redacted before trying to decrypt it. (#6203)
Check if an Event is redacted before trying to decrypt it.

Use a new enum `NotificationStatus.EventRedacted` when trying to resolve
an event for a notification.

PR done with the help of AI: GitHub copilot chat in VisualStudio.

I confirm that the fix is working in EXA with the code in
https://github.com/element-hq/element-x-android/pull/6241

Fixes #5796

<!-- description of the changes in this PR -->

- [x] I've documented the public API Changes in the appropriate
`CHANGELOG.md` files.
- [x] I've read [the `CONTRIBUTING.md`
file](https://github.com/matrix-org/matrix-rust-sdk/blob/main/CONTRIBUTING.md),
notably the sections about Pull requests, Commit message format, and AI
policy.
- [x] This PR was made with the help of AI.

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by: benoitm@element.io
2026-02-25 15:02:13 +00:00
Skye Elliot 37823df753 feat(crypto): Add room_id to RoomPendingKeyBundleDetails 2026-02-25 14:54:14 +00:00
Richard van der Hoff 096dd59c07 Add comments on race conditions 2026-02-25 14:47:56 +00:00
Richard van der Hoff 13fb3e76c5 base: switch from storing invite acceptance details in RoomInfo to crypto store 2026-02-25 14:27:18 +00:00
Richard van der Hoff 8e614c23eb base: pass the e2ee context into update_{knocked,invited}_room
... for consistency with `update_{left,joined}_room`, and because we will need
it shortly
2026-02-25 14:26:02 +00:00
Richard van der Hoff f324191c7f base: pull logic for clearing invite acceptance details up to higher level
Previously, invite acceptance details were cleared in `RoomInfo::set_state`. We
can't do that any more, because (a) that method is synchronous (b) it doesn't
have access to the crypto store (c) it would be a bit of a layering violation
even if it did.

Instead, we pull the logic up to higher-level methods which do have access to
the crypto store (though, for now, we don't use it).
2026-02-25 14:23:14 +00:00
Richard van der Hoff dc66994d66 sdk: make should_accept_bundle async
We're going to need to call async methods here, so prepare for it by making it
async
2026-02-25 14:23:14 +00:00
Richard van der Hoff 8ac5d42fbe crypto: add rooms_pending_key_bundles interface to crypto store
Add a place to store details about rooms where we are waiting for a key bundle
2026-02-25 14:23:14 +00:00
Richard van der Hoff 8f14ee976a crypto: Move InviteAcceptanceDetails to matrix-sdk-crypto, and rename
Step two in a series of refactoring: move and rename
`matrix_sdk_base::InviteAcceptanceDetails` to
`matrix_sdk_crypto::store::types::RoomPendingKeyBundleDetails`.

We're going to store this in the crypto store instead, so need to move it
down. I also want to rename it to better reflect how we interpret it.
2026-02-25 14:23:14 +00:00
Richard van der Hoff da92379e22 base: gate RoomInfo::invite_acceptance_details behind e2e-encryption
This field is only ever used when encryption is enabled, and we want to move it
to the crypto crate. So, for a starting point, gate it behind the
e2e-encryption feature flag.
2026-02-25 14:23:14 +00:00
Ivan Enderlin 0cbe48e986 feat(xtask): Add cargo xtask log timer.
This patch adds a new `timer` command. It helps to visualise the
`timer!` log in a table with their duration.
2026-02-25 15:15:36 +01:00
Ivan Enderlin 99bfb94d6d fix(xtask): Fix an expect message. 2026-02-25 15:15:36 +01:00
Ivan Enderlin 3aa424117c fix(xtask): Fix a CSS custom property naming error. 2026-02-25 15:15:36 +01:00
Ivan Enderlin 4160192709 refactor(common): Add the “Timer” prefix to timer!'s message.
The idea of adding the “Timer” prefix is to ease parsing the logs: it
gives an anchor for a regex engine (e.g. `Timer _[^_]+_ finished in`).
2026-02-25 15:15:36 +01:00
Richard van der Hoff 6bc09cbbd9 refactor(base): pass references to e2ee context instead of cloning
`matrix_sdk_base::response_processors::e2ee::E2EE` exists to pass E2EE context to the
logic within `response_processors`. Currently we pass it around by value in
many places, and hence have repetitive code for building the context, as well
as lots of calls to `.clone`.

Cloning of this type is cheap, but we can do better, by passing it around by
reference instead.
2026-02-25 15:07:27 +01:00
Doug 6c18f73b51 chore: Use multi-process lock config as the signal for enabling the OIDC cross process lock.
# Conflicts:
#	bindings/matrix-sdk-ffi/CHANGELOG.md
2026-02-24 18:11:18 +02:00
Ivan Enderlin a8e4630f56 chore(sqlite): Update deadpool to 0.13 and deadpool-sync to 0.2.
These releases include our patch to fix a panic (see
https://github.com/deadpool-rs/deadpool/pull/461).
2026-02-24 16:26:45 +01:00
Ivan Enderlin 45d16919a0 doc(ffi,sdk): Add #6174 in the CHANGELOG.md. 2026-02-24 14:08:23 +01:00
Ivan Enderlin 9fc10e90a3 doc(sdk): Add #6174 to the CHANGELOG.md. 2026-02-24 14:08:23 +01:00
Ivan Enderlin d00aa2ccd2 feat(sdk): Introduce RoomEventCache::thread_pagination. 2026-02-24 14:08:23 +01:00
Ivan Enderlin 79155bb59b refactor(sdk): Implement ThreadPagination based on Pagination. 2026-02-24 14:08:23 +01:00
Ivan Enderlin 2f26e346ed chore(sdk): Move the pagination.rs module to caches/room/pagination.rs. 2026-02-24 14:08:23 +01:00
Ivan Enderlin 2d32096534 refactor(sdk): Re-implement RoomPagination with the new Pagination type. 2026-02-24 14:08:23 +01:00
Ivan Enderlin 73814c5be0 refactor(sdk): Introduce the Pagination type along with the PaginatedCache trait. 2026-02-24 14:08:23 +01:00
Ivan Enderlin e84e83c716 doc(sdk): Fix internal doc.
This patch moves a doc paragraph from one method to another. It was
misplaced.
2026-02-24 14:08:23 +01:00
Ivan Enderlin 0edff99390 refactor(sdk): Add waited_for_initial_prev_token in LoadMoreEventsBackwardsOutcome::Gap.
This patch adds the `waited_for_initial_prev_token` field directly
inside `LoadMoreEventsBackwardsOutcome::Gap`. It removes the need
to manage the `state_guard` manually (which can be error-prone). It
also removes one access to the `state_guard` in the pagination module.
Finally, it paves the road for a shared `Pagination<C>` type for
`RoomEventCache` and `ThreadEventCache`.
2026-02-24 14:08:23 +01:00
Ivan Enderlin cdd7be13f1 refactor(sdk): Rename RoomPaginationStatus to PaginationStatus.
This patch renames the `RoomPaginationStatus` enum to `PaginationStatus`
because it won't be restricted to the `RoomEventCache` only: indeed, the
`ThreadEventCache` will soon be able to yield it too. Let's do the
renaming now.
2026-02-24 14:08:23 +01:00
Ivan Enderlin 9729e633f2 chore(sdk): Promote a debug! to error! when an HTTP request fails.
This patch increases a log from `debug` to `error` when an HTTP request
fails. This is an error, and it must appear as such in the logs.
2026-02-24 13:28:47 +01:00
dependabot[bot] f796232220 chore(deps): bump tj-actions/changed-files from 47.0.2 to 47.0.4
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.2 to 47.0.4.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v47.0.2...v47.0.4)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 13:28:29 +01:00
dependabot[bot] 13d46e6aeb chore(deps): bump CodSpeedHQ/action from 4.10.6 to 4.11.0
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.10.6 to 4.11.0.
- [Release notes](https://github.com/codspeedhq/action/releases)
- [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codspeedhq/action/compare/4deb3275dd364fb96fb074c953133d29ec96f80f...2ac572851726409c88c02a307f1ea2632a9ea59b)

---
updated-dependencies:
- dependency-name: CodSpeedHQ/action
  dependency-version: 4.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 13:27:56 +01:00
Kévin Commaille 6c75571c38 crypto: Avoid dereferencing to clone identifier
The code was dereferencing to the borrowed type, only to convert it
back to an owned type. We can just use `.clone()` for this.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-24 13:09:55 +01:00
Benjamin Bouvier ec719bbb6b refactor(send queue): monitor the send queue background task 2026-02-23 18:55:31 +02:00
Benjamin Bouvier f3cb3c7317 refactor(utd hook): monitor the background task for the UTD hook reporting 2026-02-23 18:55:31 +02:00
Benjamin Bouvier eab7dd02d2 refactor(timeline): monitor background tasks spawned by the timeline 2026-02-23 18:55:31 +02:00
Benjamin Bouvier ed8d75289e refactor(room list): monitor background loading state task 2026-02-23 18:55:31 +02:00
Benjamin Bouvier 540abe2a7c refactor(spaces): monitor background tasks spawned by the space service 2026-02-23 18:55:31 +02:00
Ivan Enderlin c017ce0928 feat(sqlite): SqliteCryptoStore has 1 write connection.
Similarly to #5382 and #5744, this patch introduces a write-only
connection in `SqliteCryptoStore`. The idea is to get many read-only
connections, and a single write-only connection behind a lock, so that
there is a single writer at a time.

This patch renames the `acquire` method to `read`, and it introduces a
new `write` connection.
2026-02-23 14:31:13 +01:00
Kévin Commaille 747b5db764 refactor: Remove unnecessary clones with .to_owned()
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 14:29:55 +01:00
Kévin Commaille 5d18820120 refactor: Use owned_*_id! macros rather than *_id!().to_owned()
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 14:29:55 +01:00
Jorge Martín 9870228a76 doc(ui): Improve logs with variable inlining 2026-02-23 12:41:57 +01:00
Jorge Martín 3c8666ae2c doc(ui): Fix comment about the event_count + invite_count check 2026-02-23 12:41:57 +01:00
Jorge Martín 3783f4925e test(ui): Try asserting we received the right invite event in test_try_sliding_sync_ignores_invites_for_non_subscribed_rooms 2026-02-23 12:41:57 +01:00
Jorge Martín 1667db12ec refactor(ui): Make MAX_SLIDING_SYNC_ATTEMPTS a const 2026-02-23 12:41:57 +01:00
Jorge Martín 8c114ad57e fix(ui): Unexpected invites are ignored for non-subscribed rooms in NotificationClient::try_sliding_sync 2026-02-23 12:41:57 +01:00
OneProg 862126800d fix: Fix a small typo in ClientBuildError::MissingHomeserver error message 2026-02-23 12:13:44 +02:00
Kévin Commaille b1e4e16f8e Add PR number to changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Kévin Commaille f283126e6e base: Remove once_cell dependency
Use the types that were stabilized in the standard library instead.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Kévin Commaille fc70a7da2c sdk: Remove once_cell dependency
Use the types that were stabilized in the standard library instead.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Kévin Commaille aff26b1ed9 ffi: Remove once_cell dependency
Use the types that were stabilized in the standard library instead.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Kévin Commaille fd5c1d847e Remove once_cell dependency
Use the types that were stabilized in the standard library instead.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Kévin Commaille 956a5d46f1 ui: Remove once_cell dependency
Use the types that were stabilized in the standard library instead.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Kévin Commaille 1dbd2caeb2 testing: Remove once_cell dependency
Use the types that were stabilized in the standard library instead.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-23 10:10:14 +00:00
Jorge Martín 4257649933 refactor(sdk): Put use behind e2e-encryption feature to appease the linter 2026-02-19 17:04:57 +01:00
Jorge Martín c996307265 doc: Add changelog entries 2026-02-19 17:04:57 +01:00
Jorge Martín fc97b04ec8 refactor(ffi): Fixed some more missing renames and added a changelog entry 2026-02-19 17:04:57 +01:00
Jorge Martín e1abe99ad0 refactor: Several renames and fixes to comments 2026-02-19 17:04:57 +01:00
Jorge Martín 07f894c2a5 refactor: Remove unrelated code added for testing 2026-02-19 17:04:57 +01:00
Jorge Martín 11f2bf0ad6 refactor: Minor improvements, some extra comments 2026-02-19 17:04:57 +01:00
Jorge Martín 54e4cb2d10 refactor: Rename CrossProcessStoreConfig into CrossProcessLockConfig, move it into matrix_sdk_common::cross_process_lock and use it inside CrossProcessLock instead of the previous lock_holder: Option<String> 2026-02-19 17:04:57 +01:00
Jorge Martín 855bdadbad doc: Fix example in docs 2026-02-19 17:04:57 +01:00
Jorge Martín 287049c719 refactor: Rename CrossProcessStoreMode to CrossProcessStoreConfig 2026-02-19 17:04:57 +01:00
Jorge Martín 12e377d296 refactor: Make CrossProcessLock act as a no-op implementation when no lock_holder is present 2026-02-19 17:04:57 +01:00
Jorge Martín 4f7f4a054f doc: More doc fixes 2026-02-19 17:04:57 +01:00
Jorge Martín c18b1cfab9 fix: Fix weird rebase issue 2026-02-19 17:04:57 +01:00
Jorge Martín d6b6eb9e31 doc: Fix documentation mentioning the removed cross process lock holder name 2026-02-19 17:04:57 +01:00
Jorge Martín 52981a839a refactor(sdk): Try making IndexedDB use CrossProcessStoreMode 2026-02-19 17:04:57 +01:00
Jorge Martín 50b1e3baf2 refactor: Use the new APIs everywhere 2026-02-19 17:04:57 +01:00
Jorge Martín 3169c65bf2 refactor(ui): Make NotificationClient use the new CrossProcessStoreMode internally 2026-02-19 17:04:57 +01:00
Jorge Martín 1b724f288f refactor(ffi): Allow creating Client instances with cross-process store mode 2026-02-19 17:04:57 +01:00
Jorge Martín def89d2661 refactor(sdk): Allow creating Client instances with cross-process store mode 2026-02-19 17:04:57 +01:00
Jorge Martín d0cce0eafe refactor(sdk-base): Allow passing a cross-process store mode for BaseClient::clone_with_in_memory_state_store 2026-02-19 17:04:57 +01:00
Jorge Martín 901e280d5c refactor(sdk-common): Allow creating dummy CrossProcessLockGuard for the CrossProcessStoreMode::SingleProcess case 2026-02-19 17:04:57 +01:00
Jorge Martín a44dc4b70c refactor(ui): Simplify EncryptionSyncService by getting rid of WithLocking, we can use CrossProcessStoreMode instead 2026-02-19 17:04:57 +01:00
Jorge Martín 7d474c1415 refactor(sdk-base): Make cross_process_lock optional for MediaStoreLock 2026-02-19 17:04:57 +01:00
Jorge Martín f95d174d9a refactor(sdk-base): Make cross_process_lock optional for EventCacheStoreLock 2026-02-19 17:04:57 +01:00
Jorge Martín 4fe4e9cb7c refactor(sdk-base): Add CrossProcessStoreMode to make cross-process lock optional 2026-02-19 17:04:57 +01:00
Ivan Enderlin 209595e66e doc: Add another checkbox about AI policy in pull_request_template.md
This patch adds a new checkbox to ensure the user has read the `CONTRIBUTING.md` file, notably the Pull requests, Commit message format, and AI policy Sections.

Signed-off-by: Ivan Enderlin <ivan@mnt.io>
2026-02-19 16:29:17 +01:00
Benjamin Bouvier 7e69880148 test: make the introduced test more resilient to races
This avoids the sleep statement by only listening to the room's event
cache, instead of listening to the background send task and then wait
for it to save the sent event in the room cache. The test is more
resilient this way.
2026-02-19 14:56:31 +01:00
Benjamin Bouvier ad1e93473d fix(event cache): don't include our own events when computing non_empty_all_duplicates
See the new paragraph in the code comment around
`non_empty_all_duplicates`.
2026-02-19 14:56:31 +01:00
Benjamin Bouvier 449fbd3ad4 test(send queue): don't ditch gaps if the sync response included only our own events 2026-02-19 14:56:31 +01:00
Benjamin Bouvier c3e3cdabf8 refactor(event cache): introduce notify_subscribers for the pinned event cache too 2026-02-18 11:35:11 +01:00
Benjamin Bouvier 258e520cee refactor(event cache): move replace_utds to the EventLinkedChunk and reuse it everywhere 2026-02-18 11:35:11 +01:00
Benjamin Bouvier 17622911d4 refactor(event cache): move find_event to the EventLinkedChunk and reuse it from everywhere 2026-02-18 11:35:11 +01:00
Benjamin Bouvier 19c22a1883 fix(event cache): have redecryption update the event-focused caches too 2026-02-18 11:35:11 +01:00
Benjamin Bouvier 188d3a2ead feat(timeline): use the event-focused caches in the timeline 2026-02-18 11:35:11 +01:00
Benjamin Bouvier 4ec7d99d71 feat(event cache): introduce the event-focused linked chunk 2026-02-18 11:35:11 +01:00
Benjamin Bouvier 01b21bd5e8 feat(linked chunk): add a new linked chunk id for event-focused timelines 2026-02-18 10:57:43 +01:00
Benjamin Bouvier a6663718d0 refactor(event factory): simplify tag() helper 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 72c6dc8e08 refactor(test): get rid of RoomAccountDataTestEvent 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 961edaf4b9 test(spaces): add an EventFactory method to create an m.space_order event 2026-02-18 10:57:04 +01:00
Benjamin Bouvier ea0a81989a refactor(test): replace usage of custom JSON with EventFactory::tag() 2026-02-18 10:57:04 +01:00
Benjamin Bouvier b68c22bd56 refactor(test): replace usage of MarkedUnread with EventFactory::marked_unread() in tests 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 130e3ad04b refactor(test): replace RoomAccountDataTestEvent in read_receipts.rs 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 9252c72dea test(timeline): replace FullyRead with EventFactory 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 6d2f787623 test(sync_builder): make add_account_data accept impl Into<Raw<...>>
Change JoinedRoomBuilder::add_account_data() to accept any type that
implements Into<Raw<AnyRoomAccountDataEvent>>, preparing for migration
away from RoomAccountDataTestEvent enum.
2026-02-18 10:57:04 +01:00
Benjamin Bouvier e3042e664b test: delete sync_events.rs
Remove the sync_events module entirely now that all usages have been
migrated to EventFactory.
2026-02-18 10:57:04 +01:00
Benjamin Bouvier e401ece78f test(room): use the EventFactory::member function instea of the fixture MEMBER event 2026-02-18 10:57:04 +01:00
Benjamin Bouvier e79654c80b test(client): replace POWER_LEVELS, NAME, TAG with EventFactory 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 0777425a73 test(notification): replace POWER_LEVELS with EventFactory 2026-02-18 10:57:04 +01:00
Benjamin Bouvier e83d085e77 test(state_store): replace sync_events usages with EventFactory 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 026acf7f45 test(indexeddb): replace MEMBER_INVITE/BAN/STRIPPED with EventFactory 2026-02-18 10:57:04 +01:00
Benjamin Bouvier ed4454fcbb test(event_factory): add presence() helper and PresenceBuilder 2026-02-18 10:57:04 +01:00
Benjamin Bouvier 3749c9c616 test(event_factory): add tag() helper for m.tag events 2026-02-18 10:57:04 +01:00
Benjamin Bouvier b95b0ba1c4 test(event_factory): add room account data support
Add support for room account data events in EventFactory:
- Add RoomAccountData format to EventFormat enum
- Add From implementations for Raw<AnyRoomAccountDataEvent>
- Add room_account_data() generic method
- Add fully_read() helper for m.fully_read events
- Add marked_unread() helper for m.marked_unread events
2026-02-18 10:57:04 +01:00
Benjamin Bouvier 4c6d5f9654 test: replace ENCRYPTION_CONTENT usages with EventFactory
Replace all usages of ENCRYPTION_CONTENT and
ENCRYPTION_WITH_ENCRYPTED_STATE_EVENTS_CONTENT static values
with EventFactory::room_encryption().into_content() calls.

This uses the new into_content() method to get just the event
content for HTTP response mocking.
2026-02-18 10:57:04 +01:00
Benjamin Bouvier 6b99f37268 test(event_factory): add into_content() method to EventBuilder
Add a generic method to EventBuilder that returns just the event
content as a serde_json::Value. This is useful when mocking HTTP
responses that return event content rather than full events.
2026-02-18 10:57:04 +01:00
Benjamin Bouvier 818d0ef233 test: remove unused static JSON values from sync_events.rs 2026-02-18 10:57:04 +01:00
Jonas Jelten 2022017b29 feat(ffi): export is_low_priority for a room
Signed-off-by: Jonas Jelten <jj@sft.lol>
2026-02-17 15:23:26 +01:00
Hugh Nimmo-Smith 3c13b3edd4 test(qr-login): assert expect error codes in grant test cases 2026-02-17 13:01:13 +01:00
Johannes Marbach 3bd3ba7aff fix(latest_event): handle edits when the target event is not directly preceding
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-02-17 10:12:21 +01:00
dependabot[bot] 9c6a3d6cf4 chore(deps): bump tj-actions/changed-files from 47.0.1 to 47.0.2
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.1 to 47.0.2.
- [Release notes](https://github.com/tj-actions/changed-files/releases)
- [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/changed-files/compare/v47.0.1...v47.0.2)

---
updated-dependencies:
- dependency-name: tj-actions/changed-files
  dependency-version: 47.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 16:56:12 +01:00
dependabot[bot] 86ae50d1bd chore(deps): bump crate-ci/typos from 1.43.4 to 1.43.5
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.4 to 1.43.5.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.43.4...v1.43.5)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.43.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 16:56:03 +01:00
Benjamin Bouvier 347d348bf4 fix(bench): add a sender to the room encryption event 2026-02-16 10:53:31 +01:00
Benjamin Bouvier 079c8175f2 refactor(test): remove the PresenceTestEvent enum that's unused 2026-02-16 10:53:31 +01:00
Benjamin Bouvier 64163c32f6 refactor(test): add an invite_room_state method to the EventBuilder
So we can pass the room state when receiving an invite, using more event
factory!
2026-02-16 10:53:31 +01:00
Benjamin Bouvier bcb84cc27c refactor(test): remove StrippedStateTestEvent 2026-02-16 10:53:31 +01:00
Benjamin Bouvier e299adbdeb refactor(test): remove more unused test event variants 2026-02-16 10:53:31 +01:00
Benjamin Bouvier 2ab1d8a985 refactor(test): remove StateTestEvent \o/\o/\o/ 2026-02-16 10:26:11 +01:00
Benjamin Bouvier d71387e7f7 refactor(test): remove StateTestEvent::Custom and replace it with usage of the event factory
There's one case where we want to create a custom join rule, and thus
must parse directly from JSON, but the rest can reuse helpers that
already exist.
2026-02-16 10:26:11 +01:00
Benjamin Bouvier fa638ae630 refactor(test): remove StateTestEvent::EncryptionWithEncryptedStateEvents 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 4965800a0a refactor(test): remove StateTestEvent::RoomName and use the event factory instead 2026-02-16 10:26:11 +01:00
Benjamin Bouvier f7b55bfb5c refactor(test): remove StateTestEvent::PowerLevels and use the event factory instead
Since it's always using the default power levels, provide a default ctor
for it, and use it throughout this commit.
2026-02-16 10:26:11 +01:00
Benjamin Bouvier 1fdf23a456 refactor(test): remove StateTestEvent::MemberInvite/Leave 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 5c1ee281a9 refactor(test): remove StateTestEvent variants that were unused 2026-02-16 10:26:11 +01:00
Benjamin Bouvier f4b401ac58 refactor(test): remove StateTestEvent::MemberAdditional and use the EventFactory instead 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 47e4c44d1b refactor(test): remove StateTestEvent::Member and use the event factory instead 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 1ba8704941 refactor(test): remove StateTestEvent::JoinRules 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 7f13f86912 refactor(test): remove StateTestEvent::HistoryVisibility 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 622825f099 refactor(test): remove StateTestEvent::Encryption 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 16e50ee484 refactor(test): remove StateTestEvent::Create and replace it with the event factory 2026-02-16 10:26:11 +01:00
Benjamin Bouvier 7dc76fa423 refactor(test): remove StateTestEvent::Aliases which was simply unused 2026-02-16 10:26:11 +01:00
Benjamin Bouvier db3308981b refactor(test): remove StateTestEvent::Alias and use the event factory instead 2026-02-16 10:26:11 +01:00
Ivan Enderlin e5cf097e32 fix(sdk): LatestEventValue::RemoteInvite is computed once per room.
An invite room is only constitued of stripped-state events. These events
do not have an `origin_server_ts` field. It means we cannot compute
the timestamp of the `LatestEventValue`. To workaround this, we set
the timestamp to `now()`. See `Builder::new_remote_for_invite` to learn
more. If an invite room receives a new event, its `LatestEventValue`'s
timestamp can be updated to `now()` again, which will make the room
bumps to the top of the room list for example. This is not an acceptable
behaviour because it can be an “attack vector”, i.e. a way to annoy
people with spammy invites. That's why, once a `RemoteInvite` has been
computed, we do not refresh it.

The fix is 2 instructions. The rest are comments and tests.
2026-02-13 11:03:24 +01:00
Benjamin Bouvier 3ac2339475 Revert "refactor: don't build the benchmarks by default either"
This reverts commit 66ea5d990d.
2026-02-12 15:16:41 +01:00
Benjamin Bouvier 9b4e60d075 fixup! refactor: don't build the benchmarks by default either 2026-02-12 14:36:45 +01:00
Benjamin Bouvier 66ea5d990d refactor: don't build the benchmarks by default either 2026-02-12 14:36:45 +01:00
Benjamin Bouvier ea40bf35b8 refactor: don't build multiverse by default 2026-02-12 14:36:45 +01:00
Stefan Ceriu 4ca1e45271 feat(ffi): replace tracing's RollingFileAppender with SizeAndDateRollingWriter (#6162)
This new file writer supports both total size and age based rolling, and
will take care of a whole class of edge cases that make rageshake
uploading fail.

* It will rotate log files when the configured time period elapses
(e.g., hourly, daily).
* When the total size of all log files exceeds the configured limit, the
oldest files are removed.
* Logs older than the configured max age are automatically removed
during cleanup.
* Only files matching the configured prefix and suffix are managed by
any given writer instance.
2026-02-12 13:28:03 +00:00
Benjamin Bouvier 771de205b3 review(event cache): rename TimelineVectorUpdate to TimelineVectorDiffs 2026-02-12 13:57:21 +01:00
Benjamin Bouvier 07fe503cd4 refactor(event cache): use TimelineVectorUpdate in the UpdateTimelineEvents variant too, for consistency 2026-02-12 13:57:21 +01:00
Benjamin Bouvier deeaa862d8 refactor(event cache): reuse the TimelineVectorUpdate for the pinned event cache
The pinned event timeline only cares about updates to the vector of
events, not the other types of updates, at the moment. As such, it
doesn't need to use the fully-fledged `RoomEventCacheUpdate`, but a
simpler version, like the one that is being used for threads. Hence, we
can reuse `TimelineVectorUpdate` here.
2026-02-12 13:57:21 +01:00
Benjamin Bouvier 91ae6a1eab refactor(event cache): move TimelineVectorUpdate to the new caches module 2026-02-12 13:57:21 +01:00
Benjamin Bouvier 86480cb37a refactor(event cache): rename ThreadEventCacheUpdate to TimelineVectorUpdate 2026-02-12 13:57:21 +01:00
Benjamin Bouvier feed4f725b fix(event cache): make sure the pinned event cache task is aborted on drop
Otherwise, it would leak if a client was killed, after it's been
created.
2026-02-12 13:57:21 +01:00
Benjamin Bouvier e7d2c729c2 chore(test): remove unused test JSONs 2026-02-12 13:00:13 +01:00
Michael Goldenberg 75d39c8186 doc(ui): move latest change log entry to the top
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-11 15:17:43 +01:00
Michael Goldenberg 726e5c95b1 doc(ui): re-word change log entry
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-11 15:17:43 +01:00
Michael Goldenberg 19572c9712 doc(ui): update change log
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-11 15:17:43 +01:00
Michael Goldenberg ce1ec8bab3 test(notification-client): replace unit test with crate-specific integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-11 15:17:43 +01:00
Michael Goldenberg 1833e9185d test(notification-client): ensure tests check for member hints in required state
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-11 15:17:43 +01:00
Michael Goldenberg d7c6af9677 test(notification-client): replace integration test with unit test
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
Co-authored-by: Jorge Martín <jorgem@element.io>
2026-02-11 15:17:43 +01:00
Michael Goldenberg 26d76d128a refactor(notification-client): add member hints to required state
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
Co-authored-by: Jorge Martín <jorgem@element.io>
2026-02-11 15:17:43 +01:00
Michael Goldenberg b35bea6c2b test(notification-client): show room names wrong in sliding sync notifications
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-11 15:17:43 +01:00
Benjamin Bouvier f69e154e30 refactor(event cache): keep the same summary check if not post-processing events after a redecryption 2026-02-11 14:56:38 +01:00
Benjamin Bouvier 4d3e7f7336 fix(event cache): have redecryption cause a thread summary update 2026-02-11 14:56:38 +01:00
Benjamin Bouvier ee1eb99134 test: write a test for a UTD as the latest thread reply
The latest thread reply in a summary is UTD, then should be resolved
automatically. It doesn't, currently, so this test acts as a regression
test.
2026-02-11 14:56:38 +01:00
Johannes Marbach 70665a84aa feat(client): make it possible to subscribe to key upload errors (#6135)
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-02-11 14:11:51 +01:00
Joe Groocock c6b58b0e2b feat: Expose MSC4171 service members
Signed-off-by: Joe Groocock <me@frebib.net>
2026-02-11 12:49:46 +00:00
Benjamin Bouvier ce0bba5e6d test: make more use of the EventFactory for the room topic
And get rid of test events. This implied adding a way to provide the
previous content of a state event, in the event factory.
2026-02-11 09:48:46 +01:00
Ivan Enderlin 87787dc04e refactor(sdk): Re-implement PinnedEventCacheStateLock with StateLock. 2026-02-11 08:23:51 +01:00
Ivan Enderlin a12f659653 refactor(sdk): Re-implement RoomEventCacheStateLock with StateLock. 2026-02-11 08:23:51 +01:00
Ivan Enderlin f4da1fa582 feat(sdk): Introduce the StateLock type in EventCache.
This patch introduces the `StateLock` type. It's a copy of the
`RoomEventCacheStateLock` logic, but generalised to any state `S`.
2026-02-11 08:23:51 +01:00
Benjamin Bouvier 7fbb2ddae1 test: add a new method for the state pinned events in the event factory 2026-02-10 18:41:15 +01:00
dependabot[bot] 1e0c930a12 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 78beac95c8fd7c25bdfb194415128523e41512d5 to 3026399e8ccbe119a9624e9376afc8c5f21fb60f.
- [Release notes](https://github.com/bnjbvr/cargo-machete/releases)
- [Changelog](https://github.com/bnjbvr/cargo-machete/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bnjbvr/cargo-machete/compare/78beac95c8fd7c25bdfb194415128523e41512d5...3026399e8ccbe119a9624e9376afc8c5f21fb60f)

---
updated-dependencies:
- dependency-name: bnjbvr/cargo-machete
  dependency-version: 3026399e8ccbe119a9624e9376afc8c5f21fb60f
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 15:50:41 +01:00
Kevin Boos 0715da0626 Enable ruma's compat-unset-avatar feature
Without this, the `Account::set_avatar_url()` function does not work
on homeservers that don't advertise support for the new endpoints
for setting profile fields.
2026-02-10 13:39:25 +01:00
dependabot[bot] b1d878ddc2 chore(deps): bump crate-ci/typos from 1.43.0 to 1.43.4
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.0 to 1.43.4.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.43.0...v1.43.4)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.43.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 13:37:59 +01:00
Ivan Enderlin 13b07c3ece feat(xtask): Always open leaf nodes.
This patch adds the `open` attribute to the `<details>` for displaying
leaf nodes of the tree. My experience shows that it's nice to always
have them open. The “occurences” list is still folded, so it shouldn't
take to much space on the screen most of the time (one line per level of
logs in this node).
2026-02-10 13:33:48 +01:00
Ivan Enderlin 00e58056a4 fix(xtask): Remove a bad calculation to timeout bar. 2026-02-10 13:33:48 +01:00
Ivan Enderlin e1e86a78c9 feat(xtask): Add log overview.
This patch adds the `log overview` task that generates a standalone HTML
report representing the logs as a tree where each node is a target, and
each leaf is a log location with occurrences, spans and fields.

Each node displays the sum of errors and warnings for this node.
It helps to quickly spot the problematic targets, and it guides to
exploration of the node without taking the reader's hand by trying to
draw conclusions. We don't want to guide the reader to a mistake: we
just want to guide the reader to draw its own conclusions.

Each line can be highlighted. When a node is folded/closed, it is also
highlighted if at least one of its child is highlighted.

The occurrences of logs are displayed on a timeline.
2026-02-10 13:33:48 +01:00
504 changed files with 41091 additions and 19470 deletions
+3
View File
@@ -11,3 +11,6 @@ rustdoc-map = true
[target.aarch64-linux-android]
# These rust flags improve the performance on Android on arm64
rustflags = ["-C", "target-feature=+neon,+aes,+sha2,+sha3,+pmuv3"]
[env]
IPHONEOS_DEPLOYMENT_TARGET = "16.0"
+2
View File
@@ -5,3 +5,5 @@ updates:
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
cooldown:
default-days: 7
+2 -1
View File
@@ -1,6 +1,7 @@
<!-- description of the changes in this PR -->
- [ ] Public API changes documented in changelogs (optional)
- [ ] I've documented the public API Changes in the appropriate `CHANGELOG.md` files.
- [ ] This PR was made with the help of AI.
<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
+9 -5
View File
@@ -11,6 +11,9 @@ jobs:
benchmarks:
name: Run Benchmarks
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
strategy:
matrix:
benchmark:
@@ -79,10 +82,12 @@ jobs:
echo "Disk space after cleanup"
df -h
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
uses: moonrepo/setup-rust@abb2d32350334249b178c401e5ec5836e0cd88d3
with:
channel: stable
cache-target: release
@@ -92,8 +97,7 @@ jobs:
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
- name: Run the benchmarks
uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30
with:
run: cargo codspeed run
mode: "instrumentation"
token: ${{ secrets.CODSPEED_TOKEN }}
mode: simulation
+42 -23
View File
@@ -12,6 +12,8 @@ on:
- synchronize
- ready_for_review
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -31,15 +33,19 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
@@ -47,12 +53,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -69,32 +75,37 @@ jobs:
steps:
- name: Checkout Rust SDK
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
ref: main
persist-credentials: false
- name: Use JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
- name: Install android sdk
uses: malinskiy/action-android/install-sdk@release/0.1.7
uses: malinskiy/action-android/install-sdk@fa103ef30331e95f266418a6a97e98f61f626887 # release/0.1.7
- name: Install android ndk
uses: nttld/setup-ndk@v1
uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
id: install-ndk
with:
ndk-version: r27
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
@@ -102,12 +113,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -136,16 +147,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Install aarch64-apple-ios target
run: rustup target install aarch64-apple-ios
@@ -156,12 +171,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
@@ -179,7 +194,7 @@ jobs:
complement-crypto:
name: "Run Complement Crypto tests"
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@399a1deeab0d7e4fa9604cbe83b1df6058c40193 # main
with:
use_rust_sdk: "." # use local checkout
use_complement_crypto: "MATCHING_BRANCH"
@@ -191,16 +206,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Add rust targets
run: |
@@ -212,7 +231,7 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
+77 -40
View File
@@ -7,6 +7,8 @@ on:
pull_request:
branches: [main]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -35,7 +37,6 @@ jobs:
- no-encryption-and-sqlite
- sqlite-cryptostore
- experimental-encrypted-state-events
- rustls-tls
- markdown
- socks
- sso-login
@@ -43,10 +44,14 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Install libsqlite
run: |
@@ -54,7 +59,7 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
@@ -65,10 +70,12 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -85,21 +92,27 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -116,7 +129,9 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install libsqlite
run: |
@@ -124,20 +139,23 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
components: clippy
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -169,10 +187,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
@@ -183,17 +203,19 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ matrix.rust }}
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Test
run: |
@@ -241,22 +263,25 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
targets: wasm32-unknown-unknown
components: clippy
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.3
uses: qmaru/wasm-pack-action@785fe709cd17eb6a97607eda9b6f5dbebed2b89c # v0.5.3
if: '!matrix.check_only'
with:
version: v0.13.1
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
@@ -267,10 +292,12 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -291,10 +318,12 @@ jobs:
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.43.0
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
lint:
name: Lint
@@ -303,26 +332,28 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: nightly-2025-10-01
toolchain: nightly-2026-02-26
components: clippy, rustfmt
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -362,7 +393,9 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install libsqlite
run: |
@@ -370,15 +403,19 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest
- name: Test
env:
@@ -390,7 +427,7 @@ jobs:
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3
with:
files: ./target/nextest/ci/junit.xml
token: ${{ secrets.CODECOV_TOKEN }}
+25 -39
View File
@@ -6,6 +6,8 @@ on:
pull_request:
branches: [main]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -24,6 +26,9 @@ jobs:
name: Code Coverage
needs: xtask
runs-on: "ubuntu-latest"
permissions:
contents: read
id-token: write
# run several docker containers with the same networking stack so the hostname 'synapse'
# maps to the synapse container, etc.
@@ -97,9 +102,10 @@ jobs:
df -h
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Install libsqlite
run: |
@@ -109,7 +115,9 @@ jobs:
sudo rm -rf /var/lib/apt/lists/*
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
# Cargo config can screw with caching and is only used for alias config
# and extra lints, which we don't care about here
@@ -117,19 +125,18 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
prefix-key: "coverage"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Install nextest and llvm-cov
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
with:
tool: nextest,cargo-llvm-cov
- name: Get xtask
uses: actions/cache/restore@v5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -148,35 +155,14 @@ jobs:
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
- name: Get PR number and commit SHA
run: |
echo "Storing PR number ${{ github.event.number }}"
echo "${{ github.event.number }}" > pr_number.txt
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
- name: Move the JUnit file into the root directory
shell: bash
run: |
mv target/nextest/ci/junit.xml ./junit.xml
# This stores the coverage report and metadata in artifacts.
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
- name: Store coverage report in artifacts
uses: actions/upload-artifact@v6
- name: Upload coverage to Codecov
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
name: codecov_report
path: |
coverage.xml
junit.xml
pr_number.txt
commit_sha.txt
if-no-files-found: error
use_oidc: true
- run: |
echo 'The coverage report was stored in Github artifacts.'
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
with:
use_oidc: true
report_type: "test_results"
+7 -2
View File
@@ -6,9 +6,14 @@ on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
permissions: {}
jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: EmbarkStudios/cargo-deny-action@v2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
+6 -2
View File
@@ -8,6 +8,8 @@ on:
pull_request: # focus on the changed files in current PR
branches: [main]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -17,10 +19,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v47.0.1
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
- name: Detect long path
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
@@ -2,11 +2,15 @@ name: Detects unused dependencies
on:
pull_request: { branches: "*" }
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Machete
uses: bnjbvr/cargo-machete@78beac95c8fd7c25bdfb194415128523e41512d5
uses: bnjbvr/cargo-machete@ac30a525c0a8d163a92d727b3ff079ee3f6ecb08
+10 -8
View File
@@ -21,25 +21,27 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Install protoc
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: nightly-2025-10-01
toolchain: nightly-2026-02-26
- name: Install Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 20
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
@@ -52,11 +54,11 @@ jobs:
- name: Upload artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: './target/doc/'
- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
+6 -2
View File
@@ -2,11 +2,15 @@ name: Git Checks
on: [pull_request]
permissions: {}
jobs:
block-fixup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
uses: 13rac1/block-fixup-merge-action@bd5504fb9ca0253e109d98eb86b7debc01970cdc # v2.0.0
+9 -3
View File
@@ -7,10 +7,16 @@ on:
pull_request:
branches: [main]
permissions: {}
jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # cargo-hack
with:
tool: cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets
-96
View File
@@ -1,96 +0,0 @@
# Copied with minimal adjustments, source:
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
name: Upload code coverage
on:
# This workflow is triggered after every successful execution
# of `coverage` workflow.
workflow_run:
workflows: ["Code Coverage"]
types:
- completed
jobs:
coverage:
name: Upload coverage report
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: 'Fetch coverage report from artifacts'
id: prepare_report
uses: actions/github-script@v8
with:
script: |
var fs = require('fs');
// List artifacts of the workflow run that triggered this workflow
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "codecov_report";
});
if (codecovReport.length != 1) {
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
}
var download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: codecovReport[0].id,
archive_format: 'zip',
});
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
- id: parse_previous_artifacts
run: |
unzip codecov_report.zip
echo "Detected PR is: $(<pr_number.txt)"
echo "Detected commit_sha is: $(<commit_sha.txt)"
# Make the params available as step output
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
# Manual overrides for these parameters are needed because automatic detection
# in codecov-action does not work for non-`pull_request` workflows.
# In `main` branch push, these default to empty strings since we want to run
# the analysis on HEAD.
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
working-directory: ${{ github.workspace }}/repo_root
# Location where coverage report files are searched for
directory: ${{ github.workspace }}
+9 -3
View File
@@ -20,6 +20,8 @@ on:
description: "The cache key for the macos build artifact"
value: "${{ jobs.xtask.outputs.cachekey-macos }}"
permissions: {}
env:
CARGO_TERM_COLOR: always
@@ -43,7 +45,9 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- name: Calculate cache key
id: cachekey
@@ -53,7 +57,7 @@ jobs:
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
- name: Check xtask cache
uses: actions/cache@v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: xtask-cache
with:
path: target/debug/xtask
@@ -64,7 +68,9 @@ jobs:
- name: Install Rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
+24
View File
@@ -0,0 +1,24 @@
name: Lint GHA workflows with zizmor
on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
permissions: {}
jobs:
zizmor:
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
+1 -1
View File
@@ -7,4 +7,4 @@ group_imports = "StdExternalCrate"
format_code_in_doc_comments = true
doc_comment_code_block_width = 80
# Workaround for https://github.com/rust-lang/rust.vim/issues/464
edition = "2021"
edition = "2024"
+3
View File
@@ -0,0 +1,3 @@
rules:
unpinned-uses:
severity: high
+15 -25
View File
@@ -81,10 +81,10 @@ contributors' pull requests and pushes.
## Pull requests
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
each commit should have a *good commit message*.
Ideally, a PR should have a _proper title_, with _atomic logical commits_, and
each commit should have a _good commit message_.
A *proper PR title* would be a one-liner summary of the changes in the PR,
A _proper PR title_ would be a one-liner summary of the changes in the PR,
following the same guidelines of a good commit message, including the
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
be a good PR title.
@@ -126,10 +126,10 @@ A good example of a changelog entry could look like the following:
For security-related changelog entries, please include the following additional
details alongside the pull request number:
* Impact: Clearly describe the issue's potential impact on users or systems.
* CVE Number: If available, include the CVE (Common Vulnerabilities and
- Impact: Clearly describe the issue's potential impact on users or systems.
- CVE Number: If available, include the CVE (Common Vulnerabilities and
Exposures) identifier.
* GitHub Advisory Link: Provide a link to the corresponding GitHub security
- GitHub Advisory Link: Provide a link to the corresponding GitHub security
advisory for further context.
```markdown
@@ -156,12 +156,12 @@ Conventional Commits are structured as follows:
The type of changes which will be included in changelogs is one of the
following:
* `feat`: A new feature
* `fix`: A bugfix
* `doc`: Documentation changes
* `refactor`: Code refactoring
* `perf`: Performance improvements
* `ci`: Changes to CI configuration files and scripts
- `feat`: A new feature
- `fix`: A bugfix
- `doc`: Documentation changes
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `ci`: Changes to CI configuration files and scripts
The scope is optional and can specify the area of the codebase affected (e.g.,
olm, cipher).
@@ -174,10 +174,10 @@ changelog entry.
The metadata must be included in the following git-trailers:
* `Security-Impact`: The magnitude of harm that can be expected, i.e.
- `Security-Impact`: The magnitude of harm that can be expected, i.e.
low/moderate/high/critical.
* `CVE`: The CVE that was assigned to this issue.
* `GitHub-Advisory`: The GitHub advisory identifier.
- `CVE`: The CVE that was assigned to this issue.
- `GitHub-Advisory`: The GitHub advisory identifier.
Please include all the fields that are available.
@@ -334,16 +334,6 @@ on Git 2.17+ you can mass signoff using rebase:
git rebase --signoff origin/main
```
## Tips for working on the `matrix-rust-sdk` with specific IDEs
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
with all features enabled, causing an error in `matrix-sdk` ("only one of the
features `native-tls` or `rustls-tls` can be enabled"). To work around this,
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
`native-tls` or `rustls-tls` feature definitions:
![Screenshot of RustRover](.img/rustrover-disable-feature.png)
## AI policy
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
Generated
+819 -484
View File
File diff suppressed because it is too large Load Diff
+17 -15
View File
@@ -11,12 +11,12 @@ members = [
"xtask",
]
exclude = ["testing/data"]
# xtask, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*", "labs/*"]
# xtask, multiverse, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*"]
resolver = "3"
[workspace.package]
rust-version = "1.88"
rust-version = "1.93"
[workspace.dependencies]
anyhow = { version = "1.0.100", default-features = false }
@@ -28,7 +28,7 @@ assert_matches2 = { version = "0.1.2", default-features = false }
async_cell = { version = "0.2.3", default-features = false }
async-compat = { version = "0.2.5", default-features = false }
async-once-cell = { version = "0.5.4", default-features = false }
async-rx = { version = "0.1.3", default-features = false }
async-rx = { version = "0.2.0", default-features = false }
# Bumping this to 0.3.6 produces a test failure because the semantic between the
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
async-stream = { version = "0.3.6", default-features = false }
@@ -46,7 +46,7 @@ eyeball-im-util = { version = "0.10.0", default-features = false }
futures-core = { version = "0.3.31", default-features = false, features = ["std"] }
futures-executor = { version = "0.3.31", default-features = false, features = ["std"] }
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
getrandom = { version = "0.2.15", default-features = false }
getrandom = { version = "0.4.2", default-features = false }
gloo-timers = { version = "0.3.0", default-features = false }
gloo-utils = { version = "0.2.0", default-features = false, features = ["serde"] }
growable-bloom-filter = { version = "2.1.1", default-features = false }
@@ -60,19 +60,20 @@ insta = { version = "1.44.1", features = ["json", "redactions"] }
itertools = { version = "0.14.0", default-features = false, features = ["use_std"] }
js-sys = { version = "0.3.82", default-features = false, features = ["std"] }
mime = { version = "0.3.17", default-features = false }
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
once_cell = { version = "1.21.3", default-features = false }
oauth2 = { version = "5.0.0", default-features = false, features = ["timing-resistant-secret-traits"] }
oauth2-reqwest = { version = "0.1.0-alpha.3", default-features = false }
pbkdf2 = { version = "0.12.2", default-features = false }
pin-project-lite = { version = "0.2.16", default-features = false }
proc-macro2 = { version = "1.0.106", default-features = false }
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
proptest = { version = "1.9.0", default-features = false, features = ["std"] }
quote = { version = "1.0.37", default-features = false }
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
rand = { version = "0.10.1", default-features = false, features = ["std", "std_rng", "thread_rng"] }
regex = { version = "1.12.2", default-features = false }
reqwest = { version = "0.12.24", default-features = false }
reqwest = { version = "0.13.1", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c15801c80b683b67dc", features = [
ruma = { git = "https://github.com/ruma/ruma", rev = "7680eebd9586669e1a4e5b1fd1c2c691221369d4", features = [
"client-api-c",
"compat-unset-avatar",
"compat-upload-signatures",
"compat-arbitrary-length-ids",
"compat-tag-info",
@@ -81,6 +82,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c158
"compat-lax-room-topic-deser",
"unstable-msc3230",
"unstable-msc3401",
"unstable-msc3417",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
@@ -93,8 +95,8 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c158
"unstable-msc4308",
"unstable-msc4310",
] }
sentry = { version = "0.46.0", default-features = false }
sentry-tracing = { version = "0.46.0", default-features = false }
sentry = { version = "0.47.0", default-features = false }
sentry-tracing = { version = "0.47.0", default-features = false }
serde = { version = "1.0.228", default-features = false, features = ["std", "rc", "derive"] }
serde_html_form = { version = "0.2.8", default-features = false }
serde_json = { version = "1.0.145", default-features = false, features = ["std"] }
@@ -117,7 +119,7 @@ uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["ca
url = { version = "2.5.7", default-features = false }
uuid = { version = "1.18.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
vodozemac = { version = "0.9.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] }
wasm-bindgen = { version = "0.2.105", default-features = false }
wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] }
web-sys = { version = "0.3.82", default-features = false }
@@ -230,7 +232,7 @@ lto = false
[patch.crates-io]
async-compat = { git = "https://github.com/element-hq/async-compat", rev = "5a27c8b290f1f1dcfc0c4ec22c464e38528aa591" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "9024a4cb3eac45c1d2d980f17aaee287b17be498" }
const_panic = { git = "https://github.com/jplatte/const_panic", rev = "e0b317a9a7bde2d48a7d15b6a60d70e4a41d3b5f" }
# Needed to fix rotation log issue on Android (https://github.com/tokio-rs/tracing/issues/2937)
tracing = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
tracing-core = { git = "https://github.com/tokio-rs/tracing.git", rev = "20f5b3d8ba057ca9c4ae00ad30dda3dce8a71c05" }
+1 -1
View File
@@ -17,7 +17,7 @@ codspeed = []
assert_matches.workspace = true
criterion = { version = "4.2.1", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
futures-util.workspace = true
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
matrix-sdk = { workspace = true, features = ["e2e-encryption", "sqlite", "testing"] }
matrix-sdk-base.workspace = true
matrix-sdk-crypto.workspace = true
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
+39 -32
View File
@@ -3,14 +3,15 @@ use std::{pin::Pin, sync::Arc};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{
RoomInfo, RoomState, SqliteEventCacheStore, StateStore,
cross_process_lock::CrossProcessLockConfig,
store::StoreConfig,
sync::{JoinedRoomUpdate, RoomUpdates},
test_utils::client::MockClientBuilder,
};
use matrix_sdk_base::event_cache::store::{DynEventCacheStore, IntoEventCacheStore, MemoryStore};
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use matrix_sdk_test::{ALICE, base64_sha256_hash, event_factory::EventFactory};
use ruma::{
EventId, RoomId, event_id,
OwnedRoomId, RoomId,
events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
room_id,
};
@@ -39,14 +40,21 @@ fn handle_room_updates(c: &mut Criterion) {
let mut changes = matrix_sdk::StateChanges::default();
for i in 0..num_rooms {
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap();
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
let raw_room_id = format!("!firstbatchroom{i:04}:example.com");
let room_id = if i % 10 == 9 {
// Make 1 in 10 rooms use a room v12 ID, which is a base64 hash similar to an
// event ID.
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
} else {
OwnedRoomId::try_from(raw_room_id).unwrap()
};
let event_factory = EventFactory::new().room(&room_id).sender(&ALICE);
let mut joined_room_update = JoinedRoomUpdate::default();
for j in 0..NUM_EVENTS {
let event_id = EventId::parse(format!("$ev{i}_{j}")).unwrap();
let event =
event_factory.text_msg(format!("Message {j}")).event_id(&event_id).into();
let event = event_factory.text_msg(format!("Message {j}")).into();
joined_room_update.timeline.events.push(event);
}
room_updates.joined.insert(room_id.clone(), joined_room_update);
@@ -102,9 +110,11 @@ fn handle_room_updates(c: &mut Criterion) {
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(state_store.clone())
.event_cache_store(event_cache_store.clone()),
StoreConfig::new(CrossProcessLockConfig::multi_process(
"cross-process-store-locks-holder-name",
))
.state_store(state_store.clone())
.event_cache_store(event_cache_store.clone()),
)
})
.build()
@@ -170,8 +180,10 @@ fn find_event_relations(c: &mut Criterion) {
let mut group = c.benchmark_group("Event cache room updates");
group.sample_size(10);
let room_id = room_id!("!room:ben.ch");
let other_room_id = room_id!("!other-room:ben.ch");
// Room v1-v11 ID.
let room_id = room_id!("!initialtestingroom:ben.ch");
// Room v12 ID.
let other_room_id = room_id!("!ICMDdumUm6RRX_eWYY2wMb2w0CY0Z_5OvlY2gBR6ELc");
// Make the state store aware of the room, so that `client.get_room()` works
// with it.
@@ -193,43 +205,37 @@ fn find_event_relations(c: &mut Criterion) {
let mut joined_room_update = JoinedRoomUpdate::default();
// Add the target event.
let target_event_id = event_id!("$target");
let target_event =
event_factory.text_msg("hello world").event_id(target_event_id).into_event();
let target_event = event_factory.text_msg("hello world").into_event();
let target_event_id =
{ &target_event.event_id().expect("generated event has an event ID") };
joined_room_update.timeline.events.push(target_event);
// Add the numerous edits.
for i in 0..num_related_events {
let event_id = EventId::parse(format!("$edit{i}")).unwrap();
let event = event_factory
.text_msg(format!("* edit {i}"))
.edit(
target_event_id,
RoomMessageEventContentWithoutRelation::text_plain(format!("edit {i}")),
)
.event_id(&event_id)
.into();
joined_room_update.timeline.events.push(event);
}
// Add other events, in the same room, without a relation.
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$msg{i}")).unwrap();
let event =
event_factory.text_msg(format!("unrelated message {i}")).event_id(&event_id).into();
let event = event_factory.text_msg(format!("unrelated message {i}")).into();
joined_room_update.timeline.events.push(event);
}
// Add other events, in the same room, related to other events.
let other_target_event_id = event_id!("$other_target");
let other_target_event =
event_factory.text_msg("hello world").event_id(other_target_event_id).into_event();
let other_target_event = event_factory.text_msg("hello world").into_event();
let other_target_event_id =
other_target_event.event_id().expect("generated event has an event ID");
joined_room_update.timeline.events.push(other_target_event);
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$unrelated{i}")).unwrap();
let event =
event_factory.reaction(other_target_event_id, "👍").event_id(&event_id).into();
for _i in 0..NUM_OTHER_EVENTS {
let event = event_factory.reaction(&other_target_event_id, "👍").into();
joined_room_update.timeline.events.push(event);
}
@@ -239,8 +245,7 @@ fn find_event_relations(c: &mut Criterion) {
let mut other_joined_room_update = JoinedRoomUpdate::default();
let event_factory = event_factory.room(other_room_id);
for i in 0..NUM_OTHER_EVENTS {
let event_id = EventId::parse(format!("$other_room{i}")).unwrap();
let event = event_factory.text_msg(format!("hi {i}")).event_id(&event_id).into();
let event = event_factory.text_msg(format!("hi {i}")).into();
other_joined_room_update.timeline.events.push(event);
}
room_updates.joined.insert(other_room_id.to_owned(), other_joined_room_update);
@@ -268,9 +273,11 @@ fn find_event_relations(c: &mut Criterion) {
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(state_store.clone())
.event_cache_store(event_cache_store),
StoreConfig::new(CrossProcessLockConfig::multi_process(
"cross-process-store-locks-holder-name",
))
.state_store(state_store.clone())
.event_cache_store(event_cache_store),
)
})
.build()
@@ -319,7 +326,7 @@ fn find_event_relations(c: &mut Criterion) {
.await
.unwrap()
.unwrap();
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
assert_eq!(target.event_id().unwrap(), *target_event_id);
assert_eq!(relations.len(), num_related_events as usize);
},
criterion::BatchSize::PerIteration,
+8 -19
View File
@@ -10,7 +10,7 @@ use matrix_sdk_base::event_cache::{
store::{DEFAULT_CHUNK_CAPACITY, DynEventCacheStore, IntoEventCacheStore, MemoryStore},
};
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use ruma::{EventId, room_id};
use ruma::room_id;
use tempfile::tempdir;
use tokio::runtime::Builder;
@@ -33,7 +33,7 @@ fn writing(c: &mut Criterion) {
.build()
.expect("Failed to create an asynchronous runtime");
let room_id = room_id!("!foo:bar.baz");
let room_id = room_id!("!fabricandofitfaber:bar.baz");
let linked_chunk_id = LinkedChunkId::Room(room_id);
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
@@ -66,12 +66,7 @@ fn writing(c: &mut Criterion) {
{
let mut events = (0..number_of_events)
.map(|nth| {
event_factory
.text_msg("foo")
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
.into_event()
})
.map(|nth| event_factory.text_msg(format!("foo {nth}")).into_event())
.peekable();
let mut gap_nth = 0;
@@ -88,9 +83,8 @@ fn writing(c: &mut Criterion) {
}
{
operations.push(Operation::PushGapBack(Gap {
prev_token: format!("gap{gap_nth}"),
}));
operations
.push(Operation::PushGapBack(Gap { token: format!("gap{gap_nth}") }));
gap_nth += 1;
}
}
@@ -150,7 +144,7 @@ fn reading(c: &mut Criterion) {
.build()
.expect("Failed to create an asynchronous runtime");
let room_id = room_id!("!foo:bar.baz");
let room_id = room_id!("!fabricandofitfaber:bar.baz");
let linked_chunk_id = LinkedChunkId::Room(room_id);
let event_factory = EventFactory::new().room(room_id).sender(&ALICE);
@@ -178,12 +172,7 @@ fn reading(c: &mut Criterion) {
// Store some events and gap chunks in the store.
{
let mut events = (0..num_events)
.map(|nth| {
event_factory
.text_msg("foo")
.event_id(&EventId::parse(format!("$ev{nth}")).unwrap())
.into_event()
})
.map(|nth| event_factory.text_msg(format!("foo {nth}")).into_event())
.peekable();
let mut lc =
@@ -198,7 +187,7 @@ fn reading(c: &mut Criterion) {
}
lc.push_items_back(events_chunk);
lc.push_gap_back(Gap { prev_token: format!("gap{num_gaps}") });
lc.push_gap_back(Gap { token: format!("gap{num_gaps}") });
num_gaps += 1;
}
+21 -29
View File
@@ -1,24 +1,24 @@
use std::time::Duration;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::{store::RoomLoadSettings, test_utils::mocks::MatrixMockServer};
use matrix_sdk::{
cross_process_lock::CrossProcessLockConfig, store::RoomLoadSettings,
test_utils::mocks::MatrixMockServer,
};
use matrix_sdk_base::{
BaseClient, RoomInfo, RoomState, SessionMeta, StateChanges, StateStore, ThreadingSupport,
store::StoreConfig,
};
use matrix_sdk_sqlite::SqliteStateStore;
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, event_factory::EventFactory};
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineFocus};
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId,
api::client::membership::get_member_events,
device_id,
events::room::member::{MembershipState, RoomMemberEvent},
mxc_uri, owned_room_id, owned_user_id,
mxc_uri, owned_device_id, owned_room_id, owned_user_id,
serde::Raw,
user_id,
};
use serde_json::json;
use tokio::runtime::Builder;
use wiremock::{Request, ResponseTemplate};
@@ -26,7 +26,7 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
const MEMBERS_IN_ROOM: usize = 100000;
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
let room_id = owned_room_id!("!room:example.com");
let room_id = owned_room_id!("!homohominilupusest:example.com");
let f = EventFactory::new().room(&room_id);
let mut member_events: Vec<Raw<RoomMemberEvent>> = Vec::with_capacity(MEMBERS_IN_ROOM);
@@ -58,16 +58,18 @@ pub fn receive_all_members_benchmark(c: &mut Criterion) {
.expect("initial filling of sqlite failed");
let base_client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(sqlite_store),
StoreConfig::new(CrossProcessLockConfig::multi_process(
"cross-process-store-locks-holder-name",
))
.state_store(sqlite_store),
ThreadingSupport::Disabled,
);
runtime
.block_on(base_client.activate(
SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
user_id: owned_user_id!("@somebody:example.com"),
device_id: owned_device_id!("DEVICE_ID"),
},
RoomLoadSettings::default(),
None,
@@ -103,32 +105,22 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
const PINNED_EVENTS_COUNT: usize = 100;
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
let room_id = owned_room_id!("!room:example.com");
let room_id = owned_room_id!("!homohominilupusest:example.com");
let sender_id = owned_user_id!("@sender:example.com");
let f = EventFactory::new().room(&room_id).sender(&sender_id);
let mut joined_room_builder =
JoinedRoomBuilder::new(&room_id).add_state_event(StateTestEvent::Encryption);
JoinedRoomBuilder::new(&room_id).add_state_event(f.room_encryption());
let pinned_event_ids: Vec<OwnedEventId> = (0..PINNED_EVENTS_COUNT)
.map(|i| EventId::parse(format!("${i}")).expect("Invalid event id"))
.map(|i| {
EventId::new_v2_or_v3(&base64_sha256_hash(format!("${i}").as_bytes()))
.expect("Invalid event id")
})
.collect();
joined_room_builder = joined_room_builder.add_state_event(StateTestEvent::Custom(json!(
{
"content": {
"pinned": pinned_event_ids
},
"event_id": "$15139375513VdeRF:localhost",
"origin_server_ts": 151393755,
"sender": "@example:localhost",
"state_key": "",
"type": "m.room.pinned_events",
"unsigned": {
"age": 703422
}
}
)));
joined_room_builder =
joined_room_builder.add_state_bulk(vec![f.room_pinned_events(pinned_event_ids).into()]);
let (server, client, room) = runtime.block_on(async move {
let server = MatrixMockServer::new().await;
+20 -9
View File
@@ -2,12 +2,12 @@ use assert_matches::assert_matches;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use futures_util::pin_mut;
use matrix_sdk::{stream::StreamExt, test_utils::mocks::MatrixMockServer};
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, event_factory::EventFactory};
use matrix_sdk_ui::{
RoomListService, eyeball_im::VectorDiff, room_list_service::filters::new_filter_non_left,
};
use rand::{distributions::Uniform, prelude::Distribution};
use ruma::{EventId, RoomId, owned_user_id};
use rand::{distr::Uniform, prelude::Distribution};
use ruma::{OwnedRoomId, RoomId, owned_user_id};
use tokio::runtime::Builder;
/// Benchmark the time it takes to create a room list.
@@ -26,19 +26,30 @@ pub fn create(c: &mut Criterion) {
});
let sender_id = owned_user_id!("@mnt_io:matrix.org");
let mut rand = rand::thread_rng();
let server_ts_range = Uniform::from(100..1000);
let mut rand = rand::rng();
let server_ts_range = Uniform::try_from(100..1000).unwrap();
for room_nth in 0..NUMBER_OF_ROOMS {
let room_id = RoomId::parse(format!("!r{room_nth}")).unwrap();
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
let raw_room_id = format!("!arsgratiaartis{room_nth:04}:example.com");
let room_id = if room_nth % 10 == 9 {
// Make 1 in 10 rooms use a room v12 ID, which is a base64 hash similar to an
// event ID.
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
} else {
OwnedRoomId::try_from(raw_room_id).unwrap()
};
let first_server_ts = server_ts_range.sample(&mut rand);
let event_factory = EventFactory::new().room(&room_id).server_ts(first_server_ts);
let events = (0..NUMBER_OF_EVENTS_PER_ROOM)
.map(|event_nth| {
let event_id = EventId::parse(format!("$ev{room_nth}_{event_nth}")).unwrap();
event_factory.text_msg("a").sender(&sender_id).event_id(&event_id).into_raw_sync()
event_factory
.text_msg(format!("a {room_nth}_{event_nth}"))
.sender(&sender_id)
.into_raw_sync()
})
.collect::<Vec<_>>();
+33 -9
View File
@@ -4,10 +4,12 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_m
use matrix_sdk::{
Client, RoomInfo, RoomState, SessionTokens, StateChanges,
authentication::matrix::MatrixSession, config::StoreConfig,
cross_process_lock::CrossProcessLockConfig,
};
use matrix_sdk_base::{SessionMeta, StateStore as _, store::MemoryStore};
use matrix_sdk_sqlite::SqliteStateStore;
use ruma::{RoomId, device_id, user_id};
use matrix_sdk_test::base64_sha256_hash;
use ruma::{OwnedRoomId, RoomId, owned_device_id, owned_user_id};
use tokio::runtime::Builder;
/// Number of joined rooms in the benchmark.
@@ -23,19 +25,37 @@ pub fn restore_session(c: &mut Criterion) {
let mut changes = StateChanges::default();
for i in 0..NUM_JOINED_ROOMS {
let room_id = RoomId::parse(format!("!room{i}:example.com")).unwrap().to_owned();
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
let raw_room_id = format!("!joinedchamber{i:05}:example.com");
let room_id = if i % 20 == 19 {
// Make 1 in 20 rooms use a room v12 ID, which is a base64 hash similar to an
// event ID.
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
} else {
OwnedRoomId::try_from(raw_room_id).unwrap()
};
changes.add_room(RoomInfo::new(&room_id, RoomState::Joined));
}
for i in 0..NUM_STRIPPED_JOINED_ROOMS {
let room_id = RoomId::parse(format!("!strippedroom{i}:example.com")).unwrap().to_owned();
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
let raw_room_id = format!("!strippedlodge{i:05}:example.com");
let room_id = if i % 20 == 19 {
// Make 1 in 20 rooms use a room v12 ID, which is a base64 hash similar to an
// event ID.
RoomId::new_v2(&base64_sha256_hash(raw_room_id.as_bytes())).unwrap()
} else {
OwnedRoomId::try_from(raw_room_id).unwrap()
};
changes.add_room(RoomInfo::new(&room_id, RoomState::Invited));
}
let session = MatrixSession {
meta: SessionMeta {
user_id: user_id!("@somebody:example.com").to_owned(),
device_id: device_id!("DEVICE_ID").to_owned(),
user_id: owned_user_id!("@somebody:example.com"),
device_id: owned_device_id!("DEVICE_ID"),
},
tokens: SessionTokens { access_token: "OHEY".to_owned(), refresh_token: None },
};
@@ -54,8 +74,10 @@ pub fn restore_session(c: &mut Criterion) {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
StoreConfig::new(CrossProcessLockConfig::multi_process(
"cross-process-store-locks-holder-name",
))
.state_store(store.clone()),
)
.build()
.await
@@ -84,8 +106,10 @@ pub fn restore_session(c: &mut Criterion) {
let client = Client::builder()
.homeserver_url("https://matrix.example.com")
.store_config(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned())
.state_store(store.clone()),
StoreConfig::new(CrossProcessLockConfig::multi_process(
"cross-process-store-locks-holder-name",
))
.state_store(store.clone()),
)
.build()
.await
+20 -22
View File
@@ -1,9 +1,9 @@
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use matrix_sdk::test_utils::mocks::MatrixMockServer;
use matrix_sdk_test::{JoinedRoomBuilder, StateTestEvent, event_factory::EventFactory};
use matrix_sdk_test::{JoinedRoomBuilder, event_factory::EventFactory};
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
use ruma::{
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
OwnedEventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
owned_user_id,
};
use tokio::runtime::Builder;
@@ -18,7 +18,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
const NUM_EVENTS: usize = 10000;
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
let room_id = owned_room_id!("!room:example.com");
let room_id = owned_room_id!("!fortesfortunajuvat:example.com");
let sender_id = owned_user_id!("@sender:example.com");
let other_sender_id = owned_user_id!("@other_sender:example.com");
@@ -35,51 +35,49 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
_ => unreachable!("math genius over here"),
};
let event_id = EventId::parse(format!("$event{i}")).unwrap();
let j = i % 10;
if j < 6 {
// Messages.
events.push(
f.text_msg(format!("Message {i}"))
.sender(sender)
.event_id(&event_id)
.into_raw_sync(),
);
events.push(f.text_msg(format!("Message {i}")).sender(sender).into_raw_sync());
} else if j < 8 {
// Reactions.
let prev_event = EventId::parse(format!("$event{}", i - 2)).unwrap();
events.push(
f.reaction(&prev_event, "👍").sender(sender).event_id(&event_id).into_raw_sync(),
);
let prev_event_id = events[i - 2]
.get_field::<OwnedEventId>("event_id")
.expect("invalid event ID")
.expect("missing event ID");
events.push(f.reaction(&prev_event_id, "👍").sender(sender).into_raw_sync());
} else if j == 8 {
// Edit.
// Note: (i-3)%3 is the same as i%3 -> same sender!
let prev_event = EventId::parse(format!("$event{}", i - 3)).unwrap();
let prev_event_id = events[i - 3]
.get_field::<OwnedEventId>("event_id")
.expect("invalid event ID")
.expect("missing event ID");
events.push(
f.text_msg(format!("* Message {}v2", i - 3))
.edit(
&prev_event,
&prev_event_id,
RoomMessageEventContentWithoutRelation::text_plain(format!(
"Message {}v2",
i - 3
)),
)
.sender(sender)
.event_id(&event_id)
.into_raw_sync(),
);
} else if j == 9 {
// Redaction.
// Note: (i-6)%3 is the same as i%6 -> same sender!
let prev_event = EventId::parse(format!("$event{}", i - 6)).unwrap();
events
.push(f.redaction(&prev_event).sender(sender).event_id(&event_id).into_raw_sync());
let prev_event_id = events[i - 6]
.get_field::<OwnedEventId>("event_id")
.expect("invalid event ID")
.expect("missing event ID");
events.push(f.redaction(&prev_event_id).sender(sender).into_raw_sync());
}
}
let builder = JoinedRoomBuilder::new(&room_id)
.add_state_event(StateTestEvent::Encryption)
.add_state_event(f.room_encryption().sender(&sender_id))
.add_timeline_bulk(events);
let room = runtime.block_on(async move {
+2 -2
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.6
// swift-tools-version:5.7
// A package manifest for local development. This file will be copied
// into the root of the repo when generating an XCFramework.
@@ -8,7 +8,7 @@ import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.iOS(.v16),
.macOS(.v12)
],
products: [
+2 -2
View File
@@ -1,4 +1,4 @@
// swift-tools-version: 5.6
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "MatrixRustSDK",
platforms: [
.iOS(.v15),
.iOS(.v16),
.macOS(.v12)
],
products: [
+1 -1
View File
@@ -2,7 +2,7 @@
name = "matrix-sdk-crypto-ffi"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2021"
edition = "2024"
rust-version.workspace = true
description = "Uniffi based bindings for the Rust SDK crypto crate"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
+24 -10
View File
@@ -33,15 +33,22 @@ Rust supports many different [targets], you'll have to make sure to pick the
right one for your device or emulator.
After this is done, we'll have to configure [Cargo] to use the correct linker
for our target. Cargo is configured using a TOML file that will be found in
`%USERPROFILE%\.cargo\config.toml` on Windows or `$HOME/.cargo/config` on Unix
platforms. More details and configuration options for Cargo can be found in the
official docs over [here](https://doc.rust-lang.org/cargo/reference/config.html).
for our target, by providing the Cargo setting of
[target.<triple>.linker](https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinker)
with a value of the path to an appropriate linker in your NDK installation.
This may be set through an environment variable:
```
$ export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="<path-to-ndk-installation>/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
```
Alternatively, it may be set in the `.cargo/config.toml` file in the current directory,
any parent directory, or your home directory:
```
[target.aarch64-linux-android]
ar = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/ar"
linker = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
linker = "<path-to-ndk-installation>/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang"
```
## Building
@@ -51,7 +58,13 @@ we'll need to set the `ANDROID_NDK` environment variable to the location of our
Android NDK installation.
```
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/22.0.7026061/
$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/<some-installed-version>
```
Also, include the NDK tools directory in your `PATH`:
```
$ export PATH="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
```
### Building for a target
@@ -62,12 +75,13 @@ The bindings can built for the `aarch64` target with:
$ cargo build --target aarch64-linux-android
```
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory.
The library will be called `libmatrix_crypto.so` and needs to be renamed and
After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory,
under the repository root directory.
The library will be called `libmatrix_sdk_crypto_ffi.so` and needs to be renamed and
copied into the `jniLibs` directory of your Android project, for Element Android:
```
$ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \
$ cp ../../target/aarch64-linux-android/debug/libmatrix_sdk_crypto_ffi.so \
/home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so
```
@@ -3,10 +3,10 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
use hmac::Hmac;
use matrix_sdk_crypto::{
backups::DecryptionError,
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
store::{CryptoStoreError as InnerStoreError, types::BackupDecryptionKey},
};
use pbkdf2::pbkdf2;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rand::{RngExt, distr::Alphanumeric, rng};
use sha2::Sha512;
use thiserror::Error;
use zeroize::Zeroize;
@@ -75,11 +75,7 @@ impl BackupRecoveryKey {
#[allow(clippy::new_without_default)]
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
inner: BackupDecryptionKey::new()
.expect("Can't gather enough randomness to create a recovery key"),
passphrase_info: None,
})
Arc::new(Self { inner: BackupDecryptionKey::new(), passphrase_info: None })
}
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
@@ -97,7 +93,7 @@ impl BackupRecoveryKey {
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
#[uniffi::constructor]
pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
let mut rng = thread_rng();
let mut rng = rng();
let salt: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
@@ -2,14 +2,14 @@ use std::{mem::ManuallyDrop, sync::Arc};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
DecryptionSettings,
dehydrated_devices::{
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
RehydratedDevice as InnerRehydratedDevice,
},
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
DecryptionSettings,
};
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
use ruma::{OwnedDeviceId, api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw};
use serde_json::json;
use crate::{CryptoStoreError, DehydratedDeviceKey};
@@ -29,8 +29,6 @@ pub enum DehydrationError {
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
PickleKeyLength(usize),
#[error(transparent)]
Rand(#[from] rand::Error),
}
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
@@ -227,13 +225,11 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
#[cfg(test)]
mod tests {
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
use crate::{DehydratedDeviceKey, dehydrated_devices::DehydrationError};
#[test]
fn test_creating_dehydrated_key() {
let result = DehydratedDeviceKey::new();
assert!(result.is_ok());
let dehydrated_device_key = result.unwrap();
let dehydrated_device_key = DehydratedDeviceKey::new();
let base_64 = dehydrated_device_key.to_base64();
let inner_bytes = dehydrated_device_key.inner;
+1 -1
View File
@@ -1,9 +1,9 @@
#![allow(missing_docs)]
use matrix_sdk_crypto::{
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
SignatureError as InnerSignatureError,
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
};
use matrix_sdk_sqlite::OpenStoreError;
use ruma::{IdParseError, OwnedUserId};
+13 -13
View File
@@ -31,22 +31,22 @@ pub use error::{
CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError,
};
use js_int::UInt;
pub use logger::{set_logger, Logger};
pub use logger::{Logger, set_logger};
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
use matrix_sdk_crypto::{
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
store::{
CryptoStore,
types::{
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
RoomSettings as RustRoomSettings,
},
CryptoStore,
},
types::{
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
},
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
};
use matrix_sdk_sqlite::SqliteCryptoStore;
pub use responses::{
@@ -54,9 +54,9 @@ pub use responses::{
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
};
use ruma::{
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
RoomId, SecondsSinceUnixEpoch, UserId,
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
};
use serde::{Deserialize, Serialize};
use tokio::runtime::Runtime;
@@ -850,9 +850,9 @@ pub struct DehydratedDeviceKey {
impl DehydratedDeviceKey {
/// Generates a new random pickle key.
pub fn new() -> Result<Self, DehydrationError> {
let inner = InnerDehydratedDeviceKey::new()?;
Ok(inner.into())
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
InnerDehydratedDeviceKey::new().into()
}
/// Creates a new dehydration pickle key from the given slice.
@@ -1015,18 +1015,18 @@ impl PkEncryption {
}
/// Encrypt a message using this [`PkEncryption`] object.
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
pub fn encrypt(&self, plaintext: &str) -> Option<PkMessage> {
use vodozemac::base64_encode;
let message = self.inner.encrypt(plaintext.as_ref());
let message = self.inner.encrypt(plaintext.as_ref()).ok()?;
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
PkMessage {
Some(PkMessage {
ciphertext: base64_encode(ciphertext),
mac: base64_encode(mac),
ephemeral_key: ephemeral_key.to_base64(),
}
})
}
}
@@ -1049,11 +1049,11 @@ uniffi::setup_scaffolding!();
#[cfg(test)]
mod tests {
use anyhow::Result;
use serde_json::{json, Value};
use serde_json::{Value, json};
use tempfile::tempdir;
use super::MigrationData;
use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings};
use crate::{EventEncryptionAlgorithm, OlmMachine, RoomSettings, migrate};
#[test]
fn android_migration() -> Result<()> {
+1 -1
View File
@@ -3,7 +3,7 @@ use std::{
sync::{Arc, Mutex},
};
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
use tracing_subscriber::{EnvFilter, fmt::MakeWriter};
/// Trait that can be used to forward Rust logs over FFI to a language specific
/// logger.
+25 -28
View File
@@ -10,6 +10,8 @@ use std::{
use js_int::UInt;
use matrix_sdk_common::deserialized_responses::AlgorithmInfo;
use matrix_sdk_crypto::{
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
UserIdentity as SdkUserIdentity,
backups::{
MegolmV1BackupKey as RustBackupKey, SignatureState,
SignatureVerification as RustSignatureCheckResult,
@@ -18,11 +20,12 @@ use matrix_sdk_crypto::{
olm::ExportedRoomKey,
store::types::{BackupDecryptionKey, Changes},
types::requests::ToDeviceRequest,
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
UserIdentity as SdkUserIdentity,
};
use ruma::{
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
api::{
IncomingResponse,
client::{
backup::add_backup_keys::v3::Response as KeysBackupResponse,
keys::{
@@ -32,38 +35,35 @@ use ruma::{
upload_signatures::v3::Response as SignatureUploadResponse,
},
message::send_message_event::v3::Response as RoomMessageResponse,
sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists},
sync::sync_events::{DeviceLists as RumaDeviceLists, v3::ToDevice},
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
},
IncomingResponse,
},
events::{
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
key::verification::VerificationMethod, room::message::MessageType,
},
serde::Raw,
to_device::DeviceIdOrAllDevices,
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{value::RawValue, Value};
use serde_json::{Value, value::RawValue};
use tokio::runtime::Runtime;
use zeroize::Zeroize;
use crate::{
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
dehydrated_devices::DehydratedDevices,
error::{
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
SignatureError,
},
parse_user_id,
responses::{response_from_string, OwnedResponse},
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
responses::{OwnedResponse, response_from_string},
};
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
@@ -913,22 +913,19 @@ impl OlmMachine {
&decryption_settings,
))?;
if handle_verification_events {
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
original_event,
)) => {
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype
{
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
}
}
_ if e.event_type().to_string().starts_with("m.key.verification") => {
if handle_verification_events
&& let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize()
{
match &e {
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(original_event)) => {
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
}
_ => (),
}
_ if e.event_type().to_string().starts_with("m.key.verification") => {
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
}
_ => (),
}
}
@@ -4,14 +4,15 @@ use std::collections::HashMap;
use http::Response;
use matrix_sdk_crypto::{
CrossSigningBootstrapRequests,
types::requests::{
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
},
CrossSigningBootstrapRequests,
};
use ruma::{
OwnedTransactionId, UserId,
api::client::{
backup::add_backup_keys::v3::Response as KeysBackupResponse,
keys::{
@@ -28,7 +29,6 @@ use ruma::{
},
assign,
events::MessageLikeEventContent,
OwnedTransactionId, UserId,
};
use serde_json::json;
+1 -1
View File
@@ -1,4 +1,4 @@
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
use matrix_sdk_crypto::{UserIdentity as SdkUserIdentity, types::CrossSigningKey};
use crate::CryptoStoreError;
@@ -3,10 +3,11 @@ use std::sync::Arc;
use futures_util::{Stream, StreamExt};
use matrix_sdk_common::executor::Handle;
use matrix_sdk_crypto::{
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
Verification as InnerVerification, VerificationRequest as InnerVerificationRequest,
CancelInfo as RustCancelInfo, QrVerification as InnerQr, QrVerificationState, Sas as InnerSas,
SasState as RustSasState, Verification as InnerVerification,
VerificationRequest as InnerVerificationRequest,
VerificationRequestState as RustVerificationRequestState,
matrix_sdk_qrcode::QrVerificationData,
};
use ruma::events::key::verification::VerificationMethod;
use vodozemac::{base64_decode, base64_encode};
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
description = "Helper macros to write FFI bindings"
edition = "2021"
edition = "2024"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "Apache-2.0"
@@ -17,9 +17,9 @@ test = false
doctest = false
[dependencies]
proc-macro2.workspace = true
proc-macro2 = { workspace = true , features = ["proc-macro"] }
quote.workspace = true
syn = { workspace = true, features = ["full", "extra-traits"] }
syn = { workspace = true, features = ["full", "extra-traits", "proc-macro"] }
[lints]
workspace = true
+8 -8
View File
@@ -27,18 +27,18 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
}
} else if let Item::Impl(blk) = &item {
for item in &blk.items {
if let ImplItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
if let ImplItem::Fn(fun) = item
&& fun.sig.asyncness.is_some()
{
return true;
}
}
} else if let Item::Trait(blk) = &item {
for item in &blk.items {
if let TraitItem::Fn(fun) = item {
if fun.sig.asyncness.is_some() {
return true;
}
if let TraitItem::Fn(fun) = item
&& fun.sig.asyncness.is_some()
{
return true;
}
}
}
+123 -1
View File
@@ -8,6 +8,19 @@ All notable changes to this project will be documented in this file.
### Bug Fixes
- Add `Client::set_avatar_url` to manually set the avatar URL of the user to a provided MXC one.
- Allow setting a custom Sliding Sync connection ID and timeline limit on `RoomListService`.
([#6289](https://github.com/matrix-org/matrix-rust-sdk/pull/6289))
- Fix devices on Android 11 crashing because the SDK could not be initialized using `libloading`
to get a reference to the JVM. Replaced `libloading` with `jvm-getter`, which works like a
compatibility layer. ([#6370](https://github.com/matrix-org/matrix-rust-sdk/pull/6370))
- Added `android_platform.rs` for fixing the `rustls` integration on Android, which was broken.
([#6306](https://github.com/matrix-org/matrix-rust-sdk/pull/6306))
- [**breaking**] `OtherState` properly supports redacted events that still have fields in the
content. The following fields are no longer optional:
- `federate` in `OtherState::RoomCreate`.
- `history_visibility` in `OtherState::RoomHistoryVisibility`.
- `thresholds` in `OtherState::RoomPowerLevels`.
- `omit_checksums` option is now enabled for the Kotlin bindings in all FFI-exporting crates.
We enabled them because with JNA direct mapping enabled they result in invalid checks in
ARM 32bit devices, preventing the SDK from working altogether (see
@@ -31,6 +44,75 @@ All notable changes to this project will be documented in this file.
### Features
- Add `Client::get_dm_rooms` function to get a list with the DMs for the provided user id.
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
- Expose `ffi::NotificationRoomInfo::service_members` so clients can use the list of service
members to calculate if a room is a DM from the notification info.
([#6474](https://github.com/matrix-org/matrix-rust-sdk/pull/6474))
- Enable `experimental-push-secrets` feature by default.
([#6473](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
searches for messages in a room or across all rooms.
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
- Added the `Client.request_openid_token()` method.
([#6458](https://github.com/matrix-org/matrix-rust-sdk/pull/6458))
- Added the `Client::import_secrets_bundle` method.
([#6212](https://github.com/matrix-org/matrix-rust-sdk/pull/6212))
- [**breaking**] Remove support for `native-tls` and remove all feature
flags for selecting TLS backend, as `rustls` is the now the only supported
TLS backend.
([#6409](https://github.com/matrix-org/matrix-rust-sdk/pull/6409))
- Expose `event_type_raw` and `latest_json()` on `EventTimelineItem`,
allowing clients to access the raw event type string and full event JSON for
custom event handling without pattern-matching through nested enums.
([#6387](https://github.com/matrix-org/matrix-rust-sdk/pull/6387))
([#6424](https://github.com/matrix-org/matrix-rust-sdk/pull/6424))
- Expose sync v2 API through FFI via `Client.sync_v2()` and
`Client.sync_once_v2()`, enabling mobile clients to sync without
requiring Sliding Sync support on the homeserver. `Client.sync_v2()`
accepts a `SyncListenerV2` callback that receives a `SyncResponseV2`
after each successful sync.
([#6359](https://github.com/matrix-org/matrix-rust-sdk/pull/6359))
- Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities
of the homeserver. ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371))
- Expose `Room.send_state_event_raw()` for sending arbitrary state events
through the FFI layer.
([#6350](https://github.com/matrix-org/matrix-rust-sdk/pull/6350))
- Introduce a `ThreadListService` which offers reactive interfaces for rendering
and managing the list of threads from a particular room.
([6311](https://github.com/matrix-org/matrix-rust-sdk/pull/6311))
- [**breaking**] Move `LiveLocation` out of `TimelineItemContent` and into `MsgLikeKind`
so it has access to `MsgLikeContent` `reactions`.
([#6286](https://github.com/matrix-org/matrix-rust-sdk/pull/6286))
- Add `HumanQrLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used to
complete a login.
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
- Add `HumanQrGrantLoginError::UnsupportedQrCodeType` for when a QR is parseable but cannot be used
to grant a login.
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6285)
- Add the `QrCodeData::base_url` and `QrCodeData::intent` methods.
([#6283](https://github.com/matrix-org/matrix-rust-sdk/pull/6283))
- Add `Encryption::recover_and_fix_backup` to automatically fix key storage backup if the
private backup decryption key is missing, invalid or inconsistent with the public key.
([#6252](https://github.com/matrix-org/matrix-rust-sdk/pull/6252))
- Add support for [MSC3489](https://github.com/matrix-org/matrix-spec-proposals/pull/3489)
live location sharing through a new `TimelineItemContent::LiveLocation` variant.
([#6232](https://github.com/matrix-org/matrix-rust-sdk/pull/6232))
- Add `HumanQrGrantLoginError::ConnectionInsecure` for errors establishing the secure channel
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
- Add `HumanQrGrantLoginError::Expired` for when a timeout is encountered during the grant
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
- Add `HumanQrGrantLoginError::Cancelled` for when the grant is cancelled
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
- Add `HumanQrGrantLoginError::OtherDeviceAlreadySignedIn` for when the other device is already signed in
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
- Add `HumanQrGrantLoginError::DeviceNotFound` for when the requested device was not returned by the homeserver
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
- Add `RoomInfo::is_low_priority` for getting the room's `m.lowpriority` tag state
([#6183](https://github.com/matrix-org/matrix-rust-sdk/pull/6183))
- Add `Client::subscribe_to_duplicate_key_upload_errors` for listening to duplicate key
upload errors from `/keys/upload`.
([#6135](https://github.com/matrix-org/matrix-rust-sdk/pull/6135/))
- Add `NotificationItem::raw_event` to get the raw event content of the event that triggered the notification, which can be useful for debugging and to support clients that want to implement custom handling for certain notifications. ([#6122](https://github.com/matrix-org/matrix-rust-sdk/pull/6122))
- [**breaking**] Extend `TimelineFocus::Event` to allow marking the target
event as the root of a thread.
@@ -71,10 +153,50 @@ All notable changes to this project will be documented in this file.
the user who forwarded the keys used to decrypt the event as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
key bundle.
([#6000](https://github.com/matrix-org/matrix-rust-sdk/pull/6000))
- Add `NonFavorite` filter to the Room List API. ([#5991](https://github.com/matrix-org/matrix-rust-sdk/pull/5991))
- Add `NonFavorite` filter to the Room List API. ([#5991](https://github.com/matrix-org/matrix-rust-sdk/pull/5991)
- Add `call_intent` (either `RtcCallIntent::Audio` or `RtcCallIntent::Video`) field to `RtcNotification` event content. ([#6207](https://github.com/matrix-org/matrix-rust-sdk/pull/6207))
- Add `RoomInfo::active_room_call_consensus_intent` method to get the call intent for the current call,
based on what members are advertising.
([#6274](https://github.com/matrix-org/matrix-rust-sdk/pull/6274))
### Refactor
- [**breaking**] `Room::observe_live_location_shares` has been replaced by
`Room::live_location_shares`. Call [`LiveLocationShares::subscribe`] on it to
receive an initial snapshot and a stream of incremental updates.The stream is seeded from the event cache
on creation and includes the own user's shares (previously excluded). `LiveLocationShare.is_live`
has been removed; instead `ts` (start timestamp) and `timeout` (duration in milliseconds) are now
exposed so clients can compute liveness themselves via `current_time < ts + timeout`. Non-live
shares are automatically removed from the list. A new `LiveLocationShareListener` callback
interface must be implemented and passed to the method.
([#6385](https://github.com/matrix-org/matrix-rust-sdk/pull/6385))
- [**breaking**] The `RoomAliases` variants of `StateEventContent`, `StateEventType` and
`OtherState` was removed. This state event type was removed from the Matrix specification a while
ago, and support for it has been removed in Ruma.
([#6414](https://github.com/matrix-org/matrix-rust-sdk/pull/6414))
- `Client::new` no longer unnecessarily instantiates an `OAuth` component if `CrossProcessLockConfig::SingleProcess`
is used. ([#6293](https://github.com/matrix-org/matrix-rust-sdk/pull/6293))
- [**breaking**] `Room::report_content()` no longer takes a `score` argument, because it was
removed from the Matrix specification.
([#6256](https://github.com/matrix-org/matrix-rust-sdk/pull/6256))
- [**breaking**] The `current_version` field of `ErrorKind::WrongRoomKeysVersion`
is no longer optional.
([#6241](https://github.com/matrix-org/matrix-rust-sdk/pull/6241))
- [**breaking**] The following variants of `AccountManagementAction` were
renamed to match their new names after being merge in the Matrix specification:
- `SessionsList` is renamed to `DevicesList`
- `SessionView` is renamed to `DeviceView`
- `SessionEnd` is renamed to `DeviceDelete`
([#6217](https://github.com/matrix-org/matrix-rust-sdk/pull/6217))
- [**breaking**] `HumanQrGrantLoginError::UnableToCreateDevice` has been removed
([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141)
- [**breaking**] Removed `ClientBuilder::enable_oidc_refresh_lock` in favour of using `ClientBuilder::cross_process_lock_config`
to configure that lock when a `MultiProcess` configuration is supplied. ([#6204](https://github.com/matrix-org/matrix-rust-sdk/pull/6204))
- `RoomPaginationStatus` is renamed to `PaginationStatus`.
([#6174](https://github.com/matrix-org/matrix-rust-sdk/pull/6174/))
- [**breaking**] Replaced `ClientBuilder::cross_process_store_locks_holder_name` with `ClientBuilder::cross_process_lock_config`,
which accepts a `CrossProcessLockConfig` value to specify whether the resulting `Client` will be used in a single
process or multiple processes. ([#6160](https://github.com/matrix-org/matrix-rust-sdk/pull/6160))
- [**breaking**] Refactored `is_last_admin` to `is_last_owner` the check will now
account also for v12 rooms, where creators and users with PL 150 matter.
([#6036](https://github.com/matrix-org/matrix-rust-sdk/pull/6036))
+14 -7
View File
@@ -1,7 +1,7 @@
[package]
name = "matrix-sdk-ffi"
version = "0.16.0"
edition = "2021"
edition = "2024"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
license = "Apache-2.0"
@@ -24,7 +24,7 @@ crate-type = [
]
[features]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets"]
# Use SQLite for the session storage.
sqlite = ["matrix-sdk/sqlite"]
# Use an embedded version of SQLite.
@@ -34,17 +34,15 @@ indexeddb = ["matrix-sdk/indexeddb"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
# Use Rustls as the TLS implementation, necessary on Android platforms.
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
sentry = ["dep:sentry", "dep:sentry-tracing"]
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
experimental-push-secrets = ["matrix-sdk/experimental-push-secrets"]
[dependencies]
anyhow.workspace = true
chrono.workspace = true
extension-trait = "1.0.2"
eyeball-im.workspace = true
futures-util.workspace = true
@@ -58,13 +56,13 @@ matrix-sdk = { workspace = true, features = [
"socks",
"uniffi",
"federation-api",
"experimental-search"
] }
matrix-sdk-base.workspace = true
matrix-sdk-common.workspace = true
matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = { version = "0.3.17", default-features = false }
once_cell.workspace = true
ruma = { workspace = true, features = [
"html",
"unstable-msc3488",
@@ -124,9 +122,18 @@ uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = { version = "0.2.2", default-features = false }
# Needed for `rustls-platform-verifier`. Newer versions aren't compatible with it.
jni = "0.21.1"
# Used to access the credential storage on Android
rustls-platform-verifier = "0.6.2"
# Needed for intializing and keeping the JavaVM reference around
once_cell = "1.21.4"
# Gobley's jvm-getter is used to get a JVM pointer from all Android versions
jvm-getter = "0.1.0"
[dev-dependencies]
similar-asserts.workspace = true
tempfile.workspace = true
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
+4 -9
View File
@@ -6,11 +6,6 @@ This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build
Given the number of platforms targeted, we have broken out a number of features
### Platform specific
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
### Functionality
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
@@ -24,11 +19,11 @@ Given the number of platforms targeted, we have broken out a number of features
## Platforms
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
Each supported target should use features to build the relevant system. Here are some suggested feature flags for the major platforms:
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
- JavaScript/Wasm: `"indexeddb,unstable-msc4274,native-tls"`
- Android: `"bundled-sqlite,unstable-msc4274,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,sentry"`
- JavaScript/Wasm: `"indexeddb,unstable-msc4274"`
### Swift/iOS sync
+14
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{
env,
error::Error,
+18 -3
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{
collections::HashMap,
fmt::{self, Debug},
@@ -5,12 +19,12 @@ use std::{
};
use matrix_sdk::{
Error,
authentication::oauth::{
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
error::OAuthAuthorizationCodeError,
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
},
Error,
};
use ruma::serde::Raw;
use url::Url;
@@ -85,8 +99,9 @@ impl SsoHandler {
let auth = self.client.inner.matrix_auth();
let url = Url::parse(&callback_url).map_err(|_| SsoError::CallbackUrlInvalid)?;
let builder =
auth.login_with_sso_callback(url).map_err(|_| SsoError::CallbackUrlInvalid)?;
auth.login_with_sso_callback(url.into()).map_err(|_| SsoError::CallbackUrlInvalid)?;
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
Ok(())
}
}
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
//! A generic `ChunkIterator` that operates over a `Vec`.
//!
//! This type is not designed to work over FFI, but it can be embedded inside an
+472 -217
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{
collections::HashMap,
fmt::Debug,
@@ -6,46 +20,48 @@ use std::{
time::Duration,
};
use anyhow::{anyhow, Context as _};
use anyhow::{Context as _, anyhow};
use futures_util::pin_mut;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
#[cfg(feature = "sqlite")]
use matrix_sdk::STATE_STORE_DATABASE_NAME;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
use matrix_sdk::{
authentication::oauth::{
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
},
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession},
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
executor::AbortOnDrop,
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
ruma::{
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
api::client::{
account::request_openid_token,
discovery::{
discover_homeserver::RtcFocusInfo,
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
},
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
room::{create_room, Visibility},
room::{Visibility, create_room},
session::get_login_types,
user_directory::search_users,
},
events::{
AnyInitialStateEvent, InitialStateEvent,
room::{
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
message::MessageType,
},
AnyInitialStateEvent, InitialStateEvent,
},
serde::Raw,
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
},
sliding_sync::Version as SdkSlidingSyncVersion,
store::RoomLoadSettings as SdkRoomLoadSettings,
sync::Notification,
task_monitor::BackgroundTaskFailureReason,
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
};
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_common::{
SendOutsideWasm, SyncOutsideWasm, cross_process_lock::CrossProcessLockConfig, stream::StreamExt,
};
use matrix_sdk_ui::{
notification_client::{
NotificationClient as MatrixNotificationClient,
@@ -57,14 +73,23 @@ use matrix_sdk_ui::{
use mime::Mime;
use oauth2::Scope;
use ruma::{
api::client::{
alias::get_alias,
OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
api::{
client::{
alias::get_alias,
discovery::get_authorization_server_metadata::v1::{
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
},
profile::{AvatarUrl, DisplayName},
room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent},
uiaa::{EmailUserIdentifier, UserIdentifier},
},
error::ErrorKind,
profile::{AvatarUrl, DisplayName},
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
uiaa::UserIdentifier,
},
events::{
AnyMessageLikeEventContent, AnySyncTimelineEvent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
direct::DirectEventContent,
fully_read::FullyReadEventContent,
identity_server::IdentityServerEventContent,
@@ -83,25 +108,22 @@ use ruma::{
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
},
tag::TagEventContent,
AnyMessageLikeEventContent, AnySyncTimelineEvent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
room::RoomType,
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use serde_json::{Value, json};
use tokio::sync::broadcast::error::RecvError;
use tracing::{debug, error};
use url::Url;
use super::{
room::{room_info::RoomInfo, Room},
room::{Room, room_info::RoomInfo},
session_verification::SessionVerificationController,
};
use crate::{
ClientError,
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
client,
encryption::Encryption,
@@ -121,10 +143,10 @@ use crate::{
runtime::get_runtime_handle,
spaces::SpaceService,
sync_service::{SyncService, SyncServiceBuilder},
sync_v2::{SyncListenerV2, SyncResponseV2, SyncSettingsV2},
task_handle::TaskHandle,
utd::{UnableToDecryptDelegate, UtdHook},
utils::AsyncRuntimeDropped,
ClientError,
};
#[derive(Clone, uniffi::Record)]
@@ -237,6 +259,32 @@ pub trait AccountDataListener: SyncOutsideWasm + SendOutsideWasm {
fn on_change(&self, event: AccountDataEvent);
}
/// A listener for duplicate key upload errors triggered by requests to
/// /keys/upload.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait DuplicateKeyUploadErrorListener: SyncOutsideWasm + SendOutsideWasm {
/// Called once when uploading keys fails.
fn on_duplicate_key_upload_error(&self, message: Option<DuplicateOneTimeKeyErrorMessage>);
}
/// Information about the old and new key that caused a duplicate key upload
/// error in /keys/upload.
#[derive(uniffi::Record)]
pub struct DuplicateOneTimeKeyErrorMessage {
/// The previously uploaded one-time key, encoded as unpadded base64.
pub old_key: String,
/// The one-time key we attempted to upload, encoded as unpadded base64
pub new_key: String,
}
impl From<matrix_sdk::encryption::DuplicateOneTimeKeyErrorMessage>
for DuplicateOneTimeKeyErrorMessage
{
fn from(value: matrix_sdk::encryption::DuplicateOneTimeKeyErrorMessage) -> Self {
Self { old_key: value.old_key.to_base64(), new_key: value.new_key.to_base64() }
}
}
/// A listener for changes of room account data events.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
@@ -269,6 +317,28 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
}
}
#[derive(Clone, uniffi::Record)]
pub struct OpenIdToken {
pub access_token: String,
pub token_type: String,
pub matrix_server_name: String,
pub expires_in_seconds: u64,
}
impl From<request_openid_token::v3::Response> for OpenIdToken {
fn from(value: request_openid_token::v3::Response) -> Self {
Self {
access_token: value.access_token,
token_type: serde_json::to_value(&value.token_type)
.ok()
.and_then(|value| value.as_str().map(ToOwned::to_owned))
.unwrap_or_else(|| format!("{:?}", value.token_type)),
matrix_server_name: value.matrix_server_name.to_string(),
expires_in_seconds: value.expires_in.as_secs(),
}
}
}
struct ClientDelegateData {
/// The delegate itself, that will receive the callbacks.
delegate: Arc<dyn ClientDelegate>,
@@ -299,7 +369,6 @@ pub struct Client {
impl Client {
pub async fn new(
sdk_client: MatrixClient,
enable_oidc_refresh_lock: bool,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
store_path: Option<PathBuf>,
) -> Result<Self, ClientError> {
@@ -322,17 +391,16 @@ impl Client {
let controller = session_verification_controller.clone();
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
if let Some(session_verification_controller) = &*controller.clone().read().await {
session_verification_controller
.process_incoming_verification_request(&event.sender, event.event_id)
.await;
}
if let MessageType::VerificationRequest(_) = &event.content.msgtype
&& let Some(session_verification_controller) = &*controller.clone().read().await
{
session_verification_controller
.process_incoming_verification_request(&event.sender, event.event_id)
.await;
}
});
let cross_process_store_locks_holder_name =
sdk_client.cross_process_store_locks_holder_name().to_owned();
let store_mode = sdk_client.cross_process_lock_config();
let client = Client {
inner: AsyncRuntimeDropped::new(sdk_client.clone()),
@@ -342,18 +410,16 @@ impl Client {
store_path,
};
if enable_oidc_refresh_lock {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
"missing session delegates when enabling the cross-process lock"
))?;
match store_mode {
CrossProcessLockConfig::MultiProcess { holder_name } => {
if session_delegate.is_none() {
return Err(anyhow::anyhow!(
"missing session delegates with multi-process lock configuration"
))?;
}
client.inner.oauth().enable_cross_process_refresh_lock(holder_name.clone()).await?;
}
client
.inner
.oauth()
.enable_cross_process_refresh_lock(cross_process_store_locks_holder_name)
.await?;
CrossProcessLockConfig::SingleProcess => {}
}
if let Some(session_delegate) = session_delegate {
@@ -448,13 +514,17 @@ impl Client {
device_id: Option<String>,
) -> Result<(), ClientError> {
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
}
if let Some(device_id) = device_id.as_ref() {
builder = builder.device_id(device_id);
}
builder.send().await?;
Ok(())
}
@@ -480,6 +550,7 @@ impl Client {
}
builder.send().await?;
Ok(())
}
@@ -494,7 +565,7 @@ impl Client {
let mut builder = self
.inner
.matrix_auth()
.login_identifier(UserIdentifier::Email { address: email }, &password);
.login_identifier(UserIdentifier::Email(EmailUserIdentifier::new(email)), &password);
if let Some(initial_device_name) = initial_device_name.as_ref() {
builder = builder.initial_device_display_name(initial_device_name);
@@ -748,6 +819,28 @@ impl Client {
})))
}
/// Subscribe to duplicate key upload errors triggered by requests to
/// /keys/upload.
pub fn subscribe_to_duplicate_key_upload_errors(
&self,
listener: Box<dyn DuplicateKeyUploadErrorListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.subscribe_to_duplicate_key_upload_errors();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
loop {
match subscriber.recv().await {
Ok(message) => {
listener.on_duplicate_key_upload_error(message.map(|m| m.into()))
}
Err(err) => {
error!("error when listening to key upload errors: {err}");
}
}
}
})))
}
/// Subscribe to updates of global account data events.
///
/// Be careful that only the most recent value can be observed. Subscribers
@@ -889,139 +982,7 @@ impl Client {
let listener = Arc::new(listener);
self.inner
.register_notification_handler(move |notification, room, _client| {
let listener = listener.clone();
let room_id = room.room_id().to_string();
async move {
// Extract information about the actions
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
// Convert SDK actions to FFI type
let actions: Vec<crate::notification_settings::Action> = notification
.actions
.into_iter()
.filter_map(|action| action.try_into().ok())
.collect();
// Convert SDK event to FFI type
let (sender, event, thread_id, raw_event) = match notification.event {
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let thread_id = match &deserialized {
AnySyncTimelineEvent::MessageLike(event) => {
match event.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(
content,
)) => match content.relates_to {
Some(Relation::Thread(thread)) => {
Some(thread.event_id.to_string())
}
_ => None,
},
_ => None,
}
}
_ => None,
};
let event = NotificationEvent::Timeline {
event: Arc::new(crate::event::TimelineEvent(Box::new(
deserialized,
))),
};
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize timeline event: {err}");
return;
}
}
}
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let event =
NotificationEvent::Invite { sender: sender.to_string() };
let thread_id = None;
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!(
"Failed to deserialize stripped state event: {err}"
);
return;
}
}
}
};
// Compile sender info
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
let sender_info = if let Some(sender) = sender.as_ref() {
NotificationSenderInfo {
display_name: sender.display_name().map(|name| name.to_owned()),
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
is_name_ambiguous: sender.name_ambiguous(),
}
} else {
NotificationSenderInfo {
display_name: None,
avatar_url: None,
is_name_ambiguous: false,
}
};
// Compile room info
let display_name = match room.display_name().await {
Ok(name) => name.to_string(),
Err(err) => {
tracing::warn!("Failed to calculate the room's display name: {err}");
return;
}
};
let is_direct = match room.is_direct().await {
Ok(is_direct) => is_direct,
Err(err) => {
tracing::warn!("Failed to determine if room is direct or not: {err}");
return;
}
};
let room_info = NotificationRoomInfo {
display_name,
avatar_url: room.avatar_url().map(Into::into),
canonical_alias: room.canonical_alias().map(Into::into),
topic: room.topic(),
join_rule: room
.join_rule()
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
joined_members_count: room.joined_members_count(),
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
};
listener.on_notification(
NotificationItem {
event,
raw_event,
sender_info,
room_info,
is_noisy: Some(is_noisy),
has_mention: Some(has_mention),
thread_id,
actions: Some(actions),
},
room_id,
);
}
notification_handler(notification, room, listener.clone())
})
.await;
}
@@ -1063,10 +1024,7 @@ impl Client {
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_well_known().await?)
}
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Retrieves a media file from the media source
///
/// Not available on Wasm platforms, due to lack of accessible file system.
@@ -1133,10 +1091,7 @@ impl Client {
Ok(())
}
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// The sliding sync version.
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
self.inner.sliding_sync_version().into()
@@ -1250,20 +1205,20 @@ impl Client {
return Ok(None);
}
let mut url_builder = match self.inner.oauth().account_management_url().await {
Ok(Some(url_builder)) => url_builder,
Ok(None) => return Ok(None),
let server_metadata = match self.inner.oauth().cached_server_metadata().await {
Ok(server_metadata) => server_metadata,
Err(e) => {
error!("Failed retrieving account management URL: {e}");
return Err(e.into());
error!("Failed retrieving cached server metadata: {e}");
return Err(OAuthError::from(e).into());
}
};
if let Some(action) = action {
url_builder = url_builder.action(action.into());
Ok(if let Some(action) = &action {
server_metadata.account_management_url_with_action(action.into())
} else {
server_metadata.account_management_uri
}
Ok(Some(url_builder.build().to_string()))
.map(Into::into))
}
pub fn user_id(&self) -> Result<String, ClientError> {
@@ -1283,12 +1238,28 @@ impl Client {
Ok(display_name)
}
pub async fn request_openid_token(&self) -> Result<OpenIdToken, ClientError> {
Ok(self.inner.account().request_openid_token().await?.into())
}
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
let mime: Mime = mime_type.parse()?;
self.inner.account().upload_avatar(&mime, data).await?;
Ok(())
}
/// Updates the user's avatar using the provided MXC url.
pub async fn set_avatar_url(&self, url: String) -> Result<(), ClientError> {
// MxcUri can't just be instantiated, serde deserialization seems to be the only
// way
let mxc = serde_json::from_str::<OwnedMxcUri>(&url)?;
// Validate the newly generated MxcUri
mxc.validate().map_err(ClientError::from_err)?;
self.inner.account().set_avatar_url(Some(&mxc)).await?;
Ok(())
}
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
self.inner.account().set_avatar_url(None).await?;
Ok(())
@@ -1512,6 +1483,7 @@ impl Client {
Ok(room)
}
/// Get the first existing DM room with the given user, if any.
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_room = self.inner.get_dm_room(&user_id);
@@ -1520,6 +1492,16 @@ impl Client {
Ok(dm)
}
/// Get an iterator with the existing DM rooms for the given user.
pub fn get_dm_rooms(&self, user_id: String) -> Result<Vec<Arc<Room>>, ClientError> {
let user_id = UserId::parse(user_id)?;
let sdk_rooms = self.inner.get_dm_rooms(&user_id);
let dms = sdk_rooms
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
.collect();
Ok(dms)
}
pub async fn search_users(
&self,
search_term: String,
@@ -1549,6 +1531,55 @@ impl Client {
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
}
/// Start a sync v2 loop.
///
/// This is an alternative to [`Client::sync_service`] (which uses Sliding
/// Sync / MSC4186). It works with any homeserver, including older
/// Synapse versions that do not support Sliding Sync.
///
/// Returns a `TaskHandle` that can be used to cancel the sync loop.
/// The listener is called after each successful sync response.
pub fn sync_v2(
&self,
settings: SyncSettingsV2,
listener: Box<dyn SyncListenerV2>,
) -> Arc<TaskHandle> {
let client = (*self.inner).clone();
let sdk_settings: matrix_sdk::config::SyncSettings = settings.into();
let listener: Arc<dyn SyncListenerV2> = Arc::from(listener);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let result = client
.sync_with_result_callback(sdk_settings, |result| {
let listener = listener.clone();
async move {
let response = result?;
let ffi_response: SyncResponseV2 = response.into();
listener.on_update(ffi_response);
Ok(matrix_sdk::LoopCtrl::Continue)
}
})
.await;
if let Err(e) = result {
tracing::error!("Sync loop ended with error: {e}");
}
})))
}
/// Perform a single sync v2 call.
///
/// This is useful for performing an initial sync or a one-shot sync
/// without entering a continuous loop.
pub async fn sync_once_v2(
&self,
settings: SyncSettingsV2,
) -> Result<SyncResponseV2, ClientError> {
let sdk_settings: matrix_sdk::config::SyncSettings = settings.into();
let response = self.inner.sync_once(sdk_settings).await?;
Ok(response.into())
}
pub async fn space_service(&self) -> Arc<SpaceService> {
let inner = UISpaceService::new((*self.inner).clone()).await;
Arc::new(SpaceService::new(inner))
@@ -2019,10 +2050,10 @@ impl Client {
let room_id = RoomId::parse(room_id)?;
// Emit the initial event, if present
if let Some(room) = self.inner.get_room(&room_id) {
if let Ok(room_info) = RoomInfo::new(&room).await {
listener.call(room_info);
}
if let Some(room) = self.inner.get_room(&room_id)
&& let Ok(room_info) = RoomInfo::new(&room).await
{
listener.call(room_info);
}
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
@@ -2034,15 +2065,150 @@ impl Client {
continue;
}
if let Some(room) = client.get_room(&room_id) {
if let Ok(room_info) = RoomInfo::new(&room).await {
listener.call(room_info);
}
if let Some(room) = client.get_room(&room_id)
&& let Ok(room_info) = RoomInfo::new(&room).await
{
listener.call(room_info);
}
}
}
}))))
}
/// Whether to enable automatic backpagination under certain conditions
/// (e.g. when processing read receipts).
///
/// This is an experimental feature, and might cause performance issues on
/// large accounts. Use with caution.
///
/// This must be called after creating a client, but before subscribing to
/// the event cache (so, before spawning a sync service or a timeline).
pub fn enable_automatic_backpagination(&self) {
self.inner.event_cache().config_mut().experimental_auto_backpagination = true;
}
pub fn homeserver_capabilities(&self) -> HomeserverCapabilities {
HomeserverCapabilities::new(self.inner.homeserver_capabilities())
}
}
async fn notification_handler(
notification: Notification,
room: matrix_sdk::Room,
listener: Arc<Box<dyn SyncNotificationListener>>,
) {
let room_id = room.room_id().to_string();
// Extract information about the actions
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
// Convert SDK actions to FFI type
let actions: Vec<crate::notification_settings::Action> =
notification.actions.into_iter().filter_map(|action| action.try_into().ok()).collect();
// Convert SDK event to FFI type
let (sender, event, thread_id, raw_event) = match notification.event {
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let thread_id = if let AnySyncTimelineEvent::MessageLike(event) = &deserialized
&& let Some(AnyMessageLikeEventContent::RoomMessage(content)) =
event.original_content()
&& let Some(Relation::Thread(thread)) = content.relates_to
{
Some(thread.event_id.to_string())
} else {
None
};
let event = NotificationEvent::Timeline {
event: Arc::new(crate::event::TimelineEvent(Box::new(deserialized))),
};
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize timeline event: {err}");
return;
}
}
}
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let event = NotificationEvent::Invite { sender: sender.to_string() };
let thread_id = None;
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize stripped state event: {err}");
return;
}
}
}
};
// Compile sender info
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
let sender_info = if let Some(sender) = sender.as_ref() {
NotificationSenderInfo {
display_name: sender.display_name().map(|name| name.to_owned()),
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
is_name_ambiguous: sender.name_ambiguous(),
}
} else {
NotificationSenderInfo { display_name: None, avatar_url: None, is_name_ambiguous: false }
};
// Compile room info
let display_name = match room.display_name().await {
Ok(name) => name.to_string(),
Err(err) => {
tracing::warn!("Failed to calculate the room's display name: {err}");
return;
}
};
let is_direct = match room.is_direct().await {
Ok(is_direct) => is_direct,
Err(err) => {
tracing::warn!("Failed to determine if room is direct or not: {err}");
return;
}
};
let room_info = NotificationRoomInfo {
display_name,
avatar_url: room.avatar_url().map(Into::into),
canonical_alias: room.canonical_alias().map(Into::into),
topic: room.topic(),
join_rule: room.join_rule().map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: room.joined_members_count(),
service_members: room
.service_members()
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
};
listener.on_notification(
NotificationItem {
event,
raw_event,
sender_info,
room_info,
is_noisy: Some(is_noisy),
has_mention: Some(has_mention),
thread_id,
actions: Some(actions),
},
room_id,
);
}
#[cfg(feature = "experimental-element-recent-emojis")]
@@ -2178,8 +2344,8 @@ impl Client {
debug!("Applying session change: {session_change:?}");
let delegate = delegate_data.delegate.clone();
get_runtime_handle().spawn_blocking(move || match session_change {
SessionChange::UnknownToken { soft_logout } => {
delegate.did_receive_auth_error(soft_logout);
SessionChange::UnknownToken(unknown_token) => {
delegate.did_receive_auth_error(unknown_token.soft_logout);
}
SessionChange::TokensRefreshed => {}
});
@@ -2599,23 +2765,23 @@ pub(crate) struct OidcSessionData {
#[derive(uniffi::Enum)]
pub enum AccountManagementAction {
Profile,
SessionsList,
SessionView { device_id: String },
SessionEnd { device_id: String },
DevicesList,
DeviceView { device_id: String },
DeviceDelete { device_id: String },
AccountDeactivate,
CrossSigningReset,
}
impl From<AccountManagementAction> for AccountManagementActionFull {
fn from(value: AccountManagementAction) -> Self {
impl<'a> From<&'a AccountManagementAction> for AccountManagementActionData<'a> {
fn from(value: &'a AccountManagementAction) -> Self {
match value {
AccountManagementAction::Profile => Self::Profile,
AccountManagementAction::SessionsList => Self::SessionsList,
AccountManagementAction::SessionView { device_id } => {
Self::SessionView { device_id: device_id.into() }
AccountManagementAction::DevicesList => Self::DevicesList,
AccountManagementAction::DeviceView { device_id } => {
Self::DeviceView(DeviceViewData::new(device_id.as_str().into()))
}
AccountManagementAction::SessionEnd { device_id } => {
Self::SessionEnd { device_id: device_id.into() }
AccountManagementAction::DeviceDelete { device_id } => {
Self::DeviceDelete(DeviceDeleteData::new(device_id.as_str().into()))
}
AccountManagementAction::AccountDeactivate => Self::AccountDeactivate,
AccountManagementAction::CrossSigningReset => Self::CrossSigningReset,
@@ -2928,16 +3094,88 @@ impl From<matrix_sdk::StoreSizes> for StoreSizes {
}
}
#[derive(uniffi::Object)]
pub struct HomeserverCapabilities {
inner: matrix_sdk::HomeserverCapabilities,
}
impl HomeserverCapabilities {
pub(crate) fn new(capabilities: matrix_sdk::HomeserverCapabilities) -> Self {
Self { inner: capabilities }
}
}
#[matrix_sdk_ffi_macros::export]
impl HomeserverCapabilities {
pub async fn refresh(&self) -> Result<(), ClientError> {
Ok(self.inner.refresh().await?)
}
pub async fn can_change_password(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_password().await?)
}
pub async fn can_change_displayname(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_displayname().await?)
}
pub async fn can_change_avatar(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_avatar().await?)
}
pub async fn can_change_thirdparty_ids(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_change_thirdparty_ids().await?)
}
pub async fn can_get_login_token(&self) -> Result<bool, ClientError> {
Ok(self.inner.can_get_login_token().await?)
}
pub async fn extended_profile_fields(&self) -> Result<ExtendedProfileFields, ClientError> {
let profile_fields = self.inner.extended_profile_fields().await?;
Ok(ExtendedProfileFields {
enabled: profile_fields.enabled,
allowed: profile_fields
.allowed
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
disallowed: profile_fields
.disallowed
.unwrap_or_default()
.iter()
.map(ToString::to_string)
.collect(),
})
}
pub async fn forgets_room_when_leaving(&self) -> Result<bool, ClientError> {
Ok(self.inner.forgets_room_when_leaving().await?)
}
}
#[derive(uniffi::Record)]
pub struct ExtendedProfileFields {
pub enabled: bool,
pub allowed: Vec<String>,
pub disallowed: Vec<String>,
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use ruma::{
api::client::room::{create_room, Visibility},
ServerName,
api::client::room::{Visibility, create_room},
authentication::TokenType,
events::StateEventType,
room::RoomType,
};
use crate::{
client::{CreateRoomParameters, JoinRule, RoomPreset, RoomVisibility},
client::{CreateRoomParameters, JoinRule, OpenIdToken, RoomPreset, RoomVisibility},
room::RoomHistoryVisibility,
};
@@ -2976,9 +3214,9 @@ mod tests {
assert_eq!(request.invite.len(), 1);
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomAvatar));
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomJoinRules));
assert!(initial_state
.iter()
.any(|e| e.event_type() == StateEventType::RoomHistoryVisibility));
assert!(
initial_state.iter().any(|e| e.event_type() == StateEventType::RoomHistoryVisibility)
);
assert_eq!(request.room_alias_name, Some("#a-room:example.com".to_owned()));
let room_type = request
@@ -2989,4 +3227,21 @@ mod tests {
.room_type;
assert_eq!(room_type, Some(RoomType::Space));
}
#[test]
fn test_openid_token_mapping() {
let response = ruma::api::client::account::request_openid_token::v3::Response::new(
"open-id-token".to_owned(),
TokenType::Bearer,
ServerName::parse("example.com").expect("valid server name"),
Duration::from_secs(3_600),
);
let token: OpenIdToken = response.into();
assert_eq!(token.access_token, "open-id-token");
assert_eq!(token.token_type, "Bearer");
assert_eq!(token.matrix_server_name, "example.com");
assert_eq!(token.expires_in_seconds, 3_600);
}
}
+129 -56
View File
@@ -1,20 +1,36 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
// Allow UniFFI to use methods marked as `#[deprecated]`.
#![allow(deprecated)]
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
#[cfg(not(target_family = "wasm"))]
#[cfg(not(any(target_family = "wasm", target_os = "android")))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, ThreadingSupport,
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
search_index::SearchIndexStoreKind,
sliding_sync::{
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, ThreadingSupport,
};
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
@@ -115,14 +131,14 @@ pub struct ClientBuilder {
homeserver_cfg: Option<HomeserverConfig>,
sliding_sync_version_builder: SlidingSyncVersionBuilder,
disable_automatic_token_refresh: bool,
cross_process_store_locks_holder_name: Option<String>,
enable_oidc_refresh_lock: bool,
cross_process_lock_config: CrossProcessLockConfig,
session_delegate: Option<Arc<dyn ClientSessionDelegate>>,
encryption_settings: EncryptionSettings,
room_key_recipient_strategy: CollectStrategy,
decryption_settings: DecryptionSettings,
enable_share_history_on_invite: bool,
request_config: Option<RequestConfig>,
search_index_store: Option<SearchIndexStoreKind>,
#[cfg(not(target_family = "wasm"))]
user_agent: Option<String>,
@@ -160,8 +176,7 @@ impl ClientBuilder {
#[cfg(not(target_family = "wasm"))]
disable_ssl_verification: false,
disable_automatic_token_refresh: false,
cross_process_store_locks_holder_name: None,
enable_oidc_refresh_lock: false,
cross_process_lock_config: CrossProcessLockConfig::SingleProcess,
session_delegate: None,
#[cfg(not(target_family = "wasm"))]
additional_root_certificates: Default::default(),
@@ -180,21 +195,16 @@ impl ClientBuilder {
enable_share_history_on_invite: false,
request_config: Default::default(),
threading_support: ThreadingSupport::Disabled,
search_index_store: None,
})
}
pub fn cross_process_store_locks_holder_name(
pub fn cross_process_lock_config(
self: Arc<Self>,
holder_name: String,
cross_process_lock_config: CrossProcessLockConfig,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cross_process_store_locks_holder_name = Some(holder_name);
Arc::new(builder)
}
pub fn enable_oidc_refresh_lock(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.enable_oidc_refresh_lock = true;
builder.cross_process_lock_config = cross_process_lock_config;
Arc::new(builder)
}
@@ -350,14 +360,41 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Set up the search index store for this client, which is used to store
/// the message search index locally.
///
/// As soon as this is enabled, messages will start to be indexed, and can
/// be later queried for search.
///
/// `path` is the directory where the search index will be stored. It must
/// be unique per session.
///
/// `password` is an optional password to encrypt the search index at rest.
/// If `None`, the search index will be stored unencrypted.
pub fn with_search_index_store(
self: Arc<Self>,
path: String,
password: Option<String>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
// Note: creation of the path is deferred to later.
let path = PathBuf::from(path);
let kind = if let Some(password) = password {
SearchIndexStoreKind::EncryptedDirectory(path, password)
} else {
SearchIndexStoreKind::UnencryptedDirectory(path)
};
builder.search_index_store = Some(kind);
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
if let Some(holder_name) = &builder.cross_process_store_locks_holder_name {
inner_builder =
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
let mut inner_builder = MatrixClient::builder()
.cross_process_store_config(builder.cross_process_lock_config.into());
let store_path = if let Some(store) = &builder.store {
match store.build()? {
@@ -382,6 +419,20 @@ impl ClientBuilder {
None
};
if let Some(search_index_store) = builder.search_index_store {
// Create the search index directory.
match search_index_store {
SearchIndexStoreKind::UnencryptedDirectory(ref path)
| SearchIndexStoreKind::EncryptedDirectory(ref path, _) => {
fs::create_dir_all(path)?;
}
_ => {}
}
// Configure the inner builder to use the search index store.
inner_builder = inner_builder.search_index_store(search_index_store);
}
// Determine server either from URL, server name or user ID.
inner_builder = match builder.homeserver_cfg {
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
@@ -406,27 +457,34 @@ impl ClientBuilder {
#[cfg(not(target_family = "wasm"))]
{
let mut certificates = Vec::new();
for certificate in builder.additional_root_certificates {
// We don't really know what type of certificate we may get here, so let's try
// first one type, then the other.
match Certificate::from_der(&certificate) {
Ok(cert) => {
certificates.push(cert);
}
Err(der_error) => {
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
ClientBuildError::Generic {
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
}
})?;
certificates.push(cert);
#[cfg(target_os = "android")]
{
inner_builder =
inner_builder.add_raw_root_certificates(builder.additional_root_certificates)
}
#[cfg(not(target_os = "android"))]
{
let mut certificates = Vec::new();
for certificate in builder.additional_root_certificates {
// We don't really know what type of certificate we may get here, so let's try
// first one type, then the other.
match Certificate::from_der(&certificate) {
Ok(cert) => {
certificates.push(cert);
}
Err(der_error) => {
let cert = Certificate::from_pem(&certificate).map_err(|pem_error| {
ClientBuildError::Generic {
message: format!("Failed to add a root certificate as DER ({der_error:?}) or PEM ({pem_error:?})"),
}
})?;
certificates.push(cert);
}
}
}
}
inner_builder = inner_builder.add_root_certificates(certificates);
inner_builder = inner_builder.add_root_certificates(certificates);
}
if builder.disable_built_in_root_certificates {
inner_builder = inner_builder.disable_built_in_root_certificates();
@@ -480,12 +538,11 @@ impl ClientBuilder {
updated_config = updated_config.timeout(Duration::from_millis(timeout));
}
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
if max_concurrent_requests > 0 {
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
max_concurrent_requests as usize,
));
}
if let Some(max_concurrent_requests) = config.max_concurrent_requests
&& max_concurrent_requests > 0
{
updated_config = updated_config
.max_concurrent_requests(NonZeroUsize::new(max_concurrent_requests as usize));
}
if let Some(max_retry_time) = config.max_retry_time {
updated_config =
@@ -498,15 +555,7 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
Ok(Arc::new(
Client::new(
sdk_client,
builder.enable_oidc_refresh_lock,
builder.session_delegate,
store_path,
)
.await?,
))
Ok(Arc::new(Client::new(sdk_client, builder.session_delegate, store_path).await?))
}
}
@@ -624,3 +673,27 @@ pub enum SlidingSyncVersionBuilder {
Native,
DiscoverNative,
}
#[derive(Clone, Debug, uniffi::Enum)]
/// The cross-process lock config to use.
pub enum CrossProcessLockConfig {
/// The client will run using multiple processes.
MultiProcess {
/// The holder name to use for the lock.
holder_name: String,
},
/// The client will run in a single process, there is no need for a
/// cross-process lock.
SingleProcess,
}
impl From<CrossProcessLockConfig> for SdkCrossProcessLockConfig {
fn from(lock_config: CrossProcessLockConfig) -> Self {
match lock_config {
CrossProcessLockConfig::MultiProcess { holder_name } => {
SdkCrossProcessLockConfig::MultiProcess { holder_name }
}
CrossProcessLockConfig::SingleProcess => SdkCrossProcessLockConfig::SingleProcess,
}
}
}
+294 -10
View File
@@ -1,11 +1,25 @@
use std::sync::Arc;
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{str::FromStr, sync::Arc};
use futures_util::StreamExt;
use matrix_sdk::{
encryption,
encryption::{backups, recovery},
};
use matrix_sdk::encryption::{self, backups, recovery};
use matrix_sdk_base::crypto::types::{BackupSecrets, RoomKeyBackupInfo};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::OwnedUserId;
use serde::de::Error;
use thiserror::Error;
use tracing::{error, info};
use zeroize::Zeroize;
@@ -224,6 +238,225 @@ impl From<encryption::VerificationState> for VerificationState {
}
}
/// Struct containing the bundle of secrets to fully activate a new device for
/// end-to-end encryption.
#[derive(uniffi::Object)]
pub struct SecretsBundleWithUserId {
user_id: OwnedUserId,
inner: matrix_sdk_base::crypto::types::SecretsBundle,
}
/// Result for the check if a store has a valid secrets bundle.
#[derive(uniffi::Enum)]
pub enum DetectedSecretsBundle {
/// The store doesn't contain a secrets bundle at all.
None,
/// The store contains a bundle without a backup.
WithoutBackup,
/// The store contains a bundle with an unused backup, the backup key in the
/// bundle isn't used on the homeserver.
UnusedBackup,
/// The store contains a complete secrets bundle.
Complete,
}
/// Error type describing failures that can happen while exporting a
/// [`SecretsBundle`] from a SQLite store.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum BundleExportError {
/// The SQLite store couldn't be opened.
#[error("the store couldn't be opened: {msg}")]
OpenStoreError { msg: String },
/// Data from the SQLite store couldn't be exported.
#[error("the bundle couldn't be exported due to a storage error: {msg}")]
StoreError { msg: String },
/// The store doesn't contain a secrets bundle or it couldn't be read from
/// the store.
#[error("the bundle couldn't be exported: {msg}")]
SecretError { msg: String },
/// The store is empty and doesn't contain a secrets bundle.
#[error("the store is completely empty")]
StoreEmpty,
/// A JSON object couldn't be deserialized while the secrets bundle was
/// exported.
#[error("Couldn't deserialize a JSON value: {msg}")]
Json { msg: String },
/// Error returned when the secrets bundle is missing a backup key or
/// includes one that doesnt match the key configured for the active backup
/// version.
#[error(
"The bundle is missing a backup key or has one that isn't the one that's currently used"
)]
InvalidBackup,
}
#[cfg(feature = "sqlite")]
impl From<matrix_sdk::encryption::BundleExportError> for BundleExportError {
fn from(value: matrix_sdk::encryption::BundleExportError) -> Self {
match value {
matrix_sdk::encryption::BundleExportError::OpenStoreError(e) => {
BundleExportError::OpenStoreError { msg: e.to_string() }
}
matrix_sdk::encryption::BundleExportError::StoreError(e) => {
BundleExportError::StoreError { msg: e.to_string() }
}
matrix_sdk::encryption::BundleExportError::SecretExport(e) => {
BundleExportError::SecretError { msg: e.to_string() }
}
}
}
}
impl From<serde_json::Error> for BundleExportError {
fn from(value: serde_json::Error) -> Self {
Self::Json { msg: value.to_string() }
}
}
#[cfg(feature = "sqlite")]
#[matrix_sdk_ffi_macros::export]
impl SecretsBundleWithUserId {
/// Attempt to export a [`SecretsBundle`] from a crypto store.
///
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
/// another [`Client`] instance.
///
/// This can be useful for migration purposes or to allow existing client
/// instances create new ones that will be fully verified.
#[uniffi::constructor]
pub async fn from_database(
database_path: &str,
mut passphrase: Option<String>,
backup_info: &str,
) -> Result<Arc<Self>, BundleExportError> {
let backup_info = serde_json::from_str(backup_info)?;
let ret = if let Some((user_id, bundle)) =
matrix_sdk::encryption::export_secrets_bundle_from_store(
database_path,
passphrase.as_deref(),
)
.await?
{
let is_backup_ok =
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
if is_backup_ok {
Ok(SecretsBundleWithUserId { user_id, inner: bundle }.into())
} else {
Err(BundleExportError::InvalidBackup)
}
} else {
Err(BundleExportError::StoreEmpty)
};
passphrase.zeroize();
ret
}
}
#[matrix_sdk_ffi_macros::export]
impl SecretsBundleWithUserId {
/// Attempt to create a [`SecretsBundle`] from a previously JSON serialized
/// bundle.
#[uniffi::constructor]
pub fn from_str(
user_id: &str,
bundle: &str,
backup_info: &str,
) -> Result<Arc<Self>, BundleExportError> {
let user_id =
OwnedUserId::from_str(user_id).map_err(|e| serde_json::Error::custom(e.to_string()))?;
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
let backup_info = serde_json::from_str(backup_info)?;
let is_backup_ok =
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
if is_backup_ok {
Ok(Self { user_id, inner: bundle }.into())
} else {
Err(BundleExportError::InvalidBackup)
}
}
/// Does the bundle contain a backup key.
///
/// Since enabling a backup is optional, the backup key might be missing
/// from the bundle. Returns `false` if the backup key is missing,
/// otherwise `true`.
pub fn contains_backup_key(&self) -> bool {
self.inner.backup.is_some()
}
}
fn is_valid_backup(secrets: &BackupSecrets, info: &RoomKeyBackupInfo) -> bool {
match secrets {
BackupSecrets::MegolmBackupV1Curve25519AesSha2(secrets) => {
secrets.key.backup_key_matches(info)
}
}
}
fn check_bundle_and_info(
bundle: &matrix_sdk_base::crypto::types::SecretsBundle,
info: Option<&RoomKeyBackupInfo>,
) -> DetectedSecretsBundle {
match (&bundle.backup, info) {
(None, None) => DetectedSecretsBundle::WithoutBackup,
(None, Some(_)) => DetectedSecretsBundle::WithoutBackup,
(Some(_), None) => DetectedSecretsBundle::UnusedBackup,
(Some(backup), Some(info)) => {
if is_valid_backup(backup, info) {
DetectedSecretsBundle::Complete
} else {
DetectedSecretsBundle::UnusedBackup
}
}
}
}
/// Check if a JSON encoded string contains a valid [`SecretsBundle`].
#[uniffi::export]
pub fn json_string_contains_secrets_bundle(
bundle: &str,
backup_info: Option<String>,
) -> Result<DetectedSecretsBundle, ClientError> {
let info: Option<RoomKeyBackupInfo> =
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
Ok(check_bundle_and_info(&bundle, info.as_ref()))
}
/// Check if a crypto store contains a valid [`SecretsBundle`].
#[cfg(feature = "sqlite")]
#[matrix_sdk_ffi_macros::export]
pub async fn database_contains_secrets_bundle(
database_path: &str,
mut passphrase: Option<String>,
backup_info: Option<String>,
) -> Result<DetectedSecretsBundle, BundleExportError> {
let info: Option<RoomKeyBackupInfo> =
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
let maybe_bundle = matrix_sdk::encryption::export_secrets_bundle_from_store(
database_path,
passphrase.as_deref(),
)
.await?;
passphrase.zeroize();
Ok(match maybe_bundle {
Some((_, bundle)) => check_bundle_and_info(&bundle, info.as_ref()),
None => DetectedSecretsBundle::None,
})
}
#[matrix_sdk_ffi_macros::export]
impl Encryption {
/// Get the public ed25519 key of our own device. This is usually what is
@@ -398,6 +631,7 @@ impl Encryption {
Ok(None)
}
/// Download identity and key backup information from Recovery
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
let result = self.inner.recovery().recover(&recovery_key).await;
@@ -406,6 +640,23 @@ impl Encryption {
Ok(result?)
}
/// Download identity and key backup information from Recovery, and, if the
/// key backup information is inconsistent, create a new key backup.
///
/// This will create a new key backup if:
///
/// * Key backup is enabled and the backup decryption key is missing from
/// Recovery, or
/// * Key backup is enabled and the backup decryption key does not match the
/// public key
pub async fn recover_and_fix_backup(&self, mut recovery_key: String) -> Result<()> {
let result = self.inner.recovery().recover_and_fix_backup(&recovery_key).await;
recovery_key.zeroize();
Ok(result?)
}
pub fn verification_state(&self) -> VerificationState {
self.inner.verification_state().get().into()
}
@@ -473,6 +724,43 @@ impl Encryption {
Ok(None)
}
}
/// This method will import all the private cross-signing keys and
/// the private part of a backup key and its accompanying version into the
/// store.
///
/// Importing all the secrets will mark the device as verified and enable
/// backups.
///
/// **Warning**: Only import this from a trusted source, i.e. if an existing
/// device is sharing this with a new device.
///
/// **Warning*: Only call this method right after logging in and before the
/// initial sync has been started.
pub async fn import_secrets_bundle(
&self,
secrets_bundle: &SecretsBundleWithUserId,
) -> Result<(), ClientError> {
let user_id = self._client.inner.user_id().expect(
"We should have a user ID available now, this is only called once we're logged in",
);
if user_id == secrets_bundle.user_id {
self.inner
.import_secrets_bundle(&secrets_bundle.inner)
.await
.map_err(ClientError::from_err)?;
self.inner.wait_for_e2ee_initialization_tasks().await;
Ok(())
} else {
Err(ClientError::Generic {
msg: "Secrets bundle does not belong to the user which was logged in".to_owned(),
details: None,
})
}
}
}
/// The E2EE identity of a user.
@@ -563,11 +851,7 @@ impl IdentityResetHandle {
/// 3. Go through the cross-signing key reset flow
/// 4. Finally, re-enable key backups only if they were enabled before
pub async fn reset(&self, auth: Option<AuthData>) -> Result<(), ClientError> {
if let Some(auth) = auth {
self.inner.reset(Some(auth.into())).await.map_err(ClientError::from_err)
} else {
self.inner.reset(None).await.map_err(ClientError::from_err)
}
self.inner.reset(auth.map(Into::into)).await.map_err(ClientError::from_err)
}
pub async fn cancel(&self) {
+87 -37
View File
@@ -1,21 +1,34 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
use matrix_sdk::{
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
authentication::oauth::OAuthError,
encryption::{identities::RequestVerificationError, CryptoStoreError},
encryption::{CryptoStoreError, identities::RequestVerificationError},
event_cache::EventCacheError,
reqwest,
room::{calls::CallError, edit::EditError},
send_queue::RoomSendQueueError,
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
use ruma::{
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
MilliSecondsSinceUnixEpoch,
api::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
};
use tracing::warn;
use uniffi::UnexpectedUniFFICallbackError;
use crate::{room_list::RoomListError, timeline::FocusEventError};
@@ -30,7 +43,6 @@ pub enum ClientError {
impl ClientError {
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
warn!("Error: {error}");
Self::Generic { msg: error.to_string(), details }
}
@@ -63,22 +75,21 @@ impl From<matrix_sdk::Error> for ClientError {
fn from(e: matrix_sdk::Error) -> Self {
match e {
matrix_sdk::Error::Http(http_error) => {
if let Some(api_error) = http_error.as_client_api_error() {
if let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
if let Some(api_error) = http_error.as_client_api_error()
&& let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
&api_error.body
{
let code = kind.errcode().to_string();
let Ok(kind) = kind.to_owned().try_into() else {
// We couldn't parse the API error, so we return a generic one instead
return (*http_error).into();
};
return Self::MatrixApi {
kind,
code,
msg: message.to_owned(),
details: Some(format!("{api_error:?}")),
};
}
{
let code = kind.errcode().to_string();
let Ok(kind) = kind.to_owned().try_into() else {
// We couldn't parse the API error, so we return a generic one instead
return (*http_error).into();
};
return Self::MatrixApi {
kind,
code,
msg: message.to_owned(),
details: Some(format!("{api_error:?}")),
};
}
(*http_error).into()
}
@@ -334,6 +345,39 @@ pub enum RoomError {
FailedSendingAttachment,
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum LiveLocationError {
#[error("Network error")]
Network,
#[error("Existing beacon information not found")]
NotFound,
#[error("Beacon event is redacted and cannot be processed")]
Redacted,
#[error("Must join the room to access beacon information")]
Stripped,
#[error("The beacon event has expired")]
NotLive,
#[error("Deserialization error")]
Deserialization,
#[error("Other error: {msg}")]
Other { msg: String },
}
impl From<matrix_sdk::BeaconError> for LiveLocationError {
fn from(value: matrix_sdk::BeaconError) -> Self {
match value {
matrix_sdk::BeaconError::Network(_) => Self::Network,
matrix_sdk::BeaconError::NotFound => Self::NotFound,
matrix_sdk::BeaconError::Redacted => Self::Redacted,
matrix_sdk::BeaconError::Stripped => Self::Stripped,
matrix_sdk::BeaconError::Deserialization(_) => Self::Deserialization,
matrix_sdk::BeaconError::NotLive => Self::NotLive,
matrix_sdk::BeaconError::Other(err) => Self::Other { msg: err.to_string() },
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum MediaInfoError {
@@ -736,7 +780,7 @@ pub enum ErrorKind {
/// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
WrongRoomKeysVersion {
/// The currently active backup version.
current_version: Option<String>,
current_version: String,
},
/// A custom API error.
@@ -750,9 +794,9 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
RumaApiErrorKind::BadAlias => Ok(ErrorKind::BadAlias),
RumaApiErrorKind::BadJson => Ok(ErrorKind::BadJson),
RumaApiErrorKind::BadState => Ok(ErrorKind::BadState),
RumaApiErrorKind::BadStatus { status, body } => Ok(ErrorKind::BadStatus {
status: status.map(|code| code.clone().as_u16()),
body: body.clone(),
RumaApiErrorKind::BadStatus(bad_status) => Ok(ErrorKind::BadStatus {
status: bad_status.status.map(|code| code.as_u16()),
body: bad_status.body.clone(),
}),
RumaApiErrorKind::CannotLeaveServerNoticeRoom => {
Ok(ErrorKind::CannotLeaveServerNoticeRoom)
@@ -764,16 +808,18 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
RumaApiErrorKind::ConnectionTimeout => Ok(ErrorKind::ConnectionTimeout),
RumaApiErrorKind::DuplicateAnnotation => Ok(ErrorKind::DuplicateAnnotation),
RumaApiErrorKind::Exclusive => Ok(ErrorKind::Exclusive),
RumaApiErrorKind::Forbidden { .. } => Ok(ErrorKind::Forbidden),
RumaApiErrorKind::Forbidden => Ok(ErrorKind::Forbidden),
RumaApiErrorKind::GuestAccessForbidden => Ok(ErrorKind::GuestAccessForbidden),
RumaApiErrorKind::IncompatibleRoomVersion { room_version } => {
Ok(ErrorKind::IncompatibleRoomVersion { room_version: room_version.to_string() })
RumaApiErrorKind::IncompatibleRoomVersion(incompatible_room_version) => {
Ok(ErrorKind::IncompatibleRoomVersion {
room_version: incompatible_room_version.room_version.to_string(),
})
}
RumaApiErrorKind::InvalidParam => Ok(ErrorKind::InvalidParam),
RumaApiErrorKind::InvalidRoomState => Ok(ErrorKind::InvalidRoomState),
RumaApiErrorKind::InvalidUsername => Ok(ErrorKind::InvalidUsername),
RumaApiErrorKind::LimitExceeded { retry_after } => {
let retry_after_ms = match retry_after {
RumaApiErrorKind::LimitExceeded(limit_exceeded) => {
let retry_after_ms = match &limit_exceeded.retry_after {
Some(RetryAfter::Delay(duration)) => Some(duration.as_millis() as u64),
Some(RetryAfter::DateTime(system_time)) => {
let duration = MilliSecondsSinceUnixEpoch::now()
@@ -790,8 +836,10 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
RumaApiErrorKind::NotFound => Ok(ErrorKind::NotFound),
RumaApiErrorKind::NotJson => Ok(ErrorKind::NotJson),
RumaApiErrorKind::NotYetUploaded => Ok(ErrorKind::NotYetUploaded),
RumaApiErrorKind::ResourceLimitExceeded { admin_contact } => {
Ok(ErrorKind::ResourceLimitExceeded { admin_contact: admin_contact.to_owned() })
RumaApiErrorKind::ResourceLimitExceeded(resource_limit_exceeded) => {
Ok(ErrorKind::ResourceLimitExceeded {
admin_contact: resource_limit_exceeded.admin_contact.clone(),
})
}
RumaApiErrorKind::RoomInUse => Ok(ErrorKind::RoomInUse),
RumaApiErrorKind::ServerNotTrusted => Ok(ErrorKind::ServerNotTrusted),
@@ -807,8 +855,8 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
RumaApiErrorKind::UnableToGrantJoin => Ok(ErrorKind::UnableToGrantJoin),
RumaApiErrorKind::Unauthorized => Ok(ErrorKind::Unauthorized),
RumaApiErrorKind::Unknown => Ok(ErrorKind::Unknown),
RumaApiErrorKind::UnknownToken { soft_logout } => {
Ok(ErrorKind::UnknownToken { soft_logout: soft_logout.to_owned() })
RumaApiErrorKind::UnknownToken(unknown_token) => {
Ok(ErrorKind::UnknownToken { soft_logout: unknown_token.soft_logout.to_owned() })
}
RumaApiErrorKind::Unrecognized => Ok(ErrorKind::Unrecognized),
RumaApiErrorKind::UnsupportedRoomVersion => Ok(ErrorKind::UnsupportedRoomVersion),
@@ -818,10 +866,12 @@ impl TryFrom<RumaApiErrorKind> for ErrorKind {
RumaApiErrorKind::UserLocked => Ok(ErrorKind::UserLocked),
RumaApiErrorKind::UserSuspended => Ok(ErrorKind::UserSuspended),
RumaApiErrorKind::WeakPassword => Ok(ErrorKind::WeakPassword),
RumaApiErrorKind::WrongRoomKeysVersion { current_version } => {
Ok(ErrorKind::WrongRoomKeysVersion { current_version: current_version.to_owned() })
RumaApiErrorKind::WrongRoomKeysVersion(wrong_version) => {
Ok(ErrorKind::WrongRoomKeysVersion {
current_version: wrong_version.current_version.clone(),
})
}
RumaApiErrorKind::_Custom { .. } => {
RumaApiErrorKind::_Custom(_) => {
// There is no way to map the extra values since they're private, so we omit
// them
Ok(ErrorKind::Custom { errcode: value.errcode().to_string() })
+28 -17
View File
@@ -1,26 +1,40 @@
use anyhow::{bail, Context};
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use anyhow::{Context, bail};
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
EventId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
TimelineEventType as RumaTimelineEventType,
room::{
encrypted,
message::{MessageType as RumaMessageType, Relation},
redaction::SyncRoomRedactionEvent,
},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
TimelineEventType as RumaTimelineEventType,
},
EventId,
};
use crate::{
room_member::MembershipState,
ruma::{MessageType, RtcNotificationType},
utils::Timestamp,
ClientError,
room_member::MembershipState,
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
utils::Timestamp,
};
#[derive(uniffi::Object)]
@@ -75,6 +89,7 @@ impl From<AnyTimelineEvent> for TimelineEvent {
/// The timeline event type.
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
#[uniffi::export(Eq, Hash)]
pub enum TimelineEventType {
/// The event is a message-like one and should be displayed as such.
MessageLike { value: MessageLikeEventType },
@@ -210,9 +225,6 @@ impl From<RumaTimelineEventType> for TimelineEventType {
RumaTimelineEventType::PolicyRuleUser => {
Self::State { value: StateEventType::PolicyRuleUser }
}
RumaTimelineEventType::RoomAliases => {
Self::State { value: StateEventType::RoomAliases }
}
RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar },
RumaTimelineEventType::RoomCanonicalAlias => {
Self::State { value: StateEventType::RoomCanonicalAlias }
@@ -292,7 +304,6 @@ pub enum StateEventContent {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar,
RoomCanonicalAlias,
RoomCreate,
@@ -320,7 +331,6 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
AnySyncStateEvent::PolicyRuleRoom(_) => StateEventContent::PolicyRuleRoom,
AnySyncStateEvent::PolicyRuleServer(_) => StateEventContent::PolicyRuleServer,
AnySyncStateEvent::PolicyRuleUser(_) => StateEventContent::PolicyRuleUser,
AnySyncStateEvent::RoomAliases(_) => StateEventContent::RoomAliases,
AnySyncStateEvent::RoomAvatar(_) => StateEventContent::RoomAvatar,
AnySyncStateEvent::RoomCanonicalAlias(_) => StateEventContent::RoomCanonicalAlias,
AnySyncStateEvent::RoomCreate(_) => StateEventContent::RoomCreate,
@@ -371,6 +381,8 @@ pub enum MessageLikeEventContent {
notification_type: RtcNotificationType,
/// The timestamp at which this notification is considered invalid.
expiration_ts: Timestamp,
/// Soft indication of whether it is an audio or video call.
call_intent: Option<RtcCallIntent>,
},
CallHangup,
CallCandidates,
@@ -413,6 +425,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
MessageLikeEventContent::RtcNotification {
notification_type: original_content.notification_type.into(),
expiration_ts,
call_intent: original_content.call_intent.map(|intent| intent.into()),
}
}
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
@@ -455,7 +468,7 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
let original_content = get_message_like_event_original_content(content)?;
let in_reply_to_event_id =
original_content.relates_to.and_then(|relation| match relation {
Relation::Reply { in_reply_to } => Some(in_reply_to.event_id.to_string()),
Relation::Reply(reply) => Some(reply.in_reply_to.event_id.to_string()),
_ => None,
});
MessageLikeEventContent::RoomMessage {
@@ -509,7 +522,6 @@ pub enum StateEventType {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar,
RoomCanonicalAlias,
RoomCreate,
@@ -541,7 +553,6 @@ impl From<StateEventType> for ruma::events::StateEventType {
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
StateEventType::RoomAliases => Self::RoomAliases,
StateEventType::RoomAvatar => Self::RoomAvatar,
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
StateEventType::RoomCreate => Self::RoomCreate,
+14
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
pub(crate) fn unwrap_or_clone_arc<T: Clone>(arc: Arc<T>) -> T {
+2 -1
View File
@@ -24,13 +24,14 @@ mod room_member;
mod room_preview;
mod ruma;
mod runtime;
mod search;
mod session_verification;
mod spaces;
mod store;
mod sync_service;
mod sync_v2;
mod task_handle;
mod timeline;
mod tracing;
mod utd;
mod utils;
mod widget;
@@ -9,24 +9,156 @@
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// See the License for that specific language governing permissions and
// limitations under the License.
use crate::ruma::LocationContent;
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt as _;
use matrix_sdk::live_location_share::{
LiveLocationShare as SdkLiveLocationShare, LiveLocationShares as SdkLiveLocationShares,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use crate::{ruma::LocationContent, runtime::get_runtime_handle, task_handle::TaskHandle};
/// Details of the last known location beacon.
#[derive(uniffi::Record)]
pub struct LastLocation {
/// The most recent location content of the user.
/// The most recent location content shared for this asset.
pub location: LocationContent,
/// A timestamp in milliseconds since Unix Epoch on that day in local
/// time.
/// The timestamp of when the location was updated.
pub ts: u64,
}
/// Details of a users live location share.
/// Details of a user's live location share.
#[derive(uniffi::Record)]
pub struct LiveLocationShare {
/// The user's last known location.
pub last_location: LastLocation,
/// The live status of the live location share.
pub(crate) is_live: bool,
/// The asset's last known location.
pub last_location: Option<LastLocation>,
/// The user ID of the person sharing their live location.
pub user_id: String,
/// The time when location sharing started.
pub start_ts: u64,
/// The duration that the location sharing will be live.
/// Meaning that the location will stop being shared at ts + timeout.
pub timeout: u64,
}
/// An update to the list of active live location shares.
///
/// Corresponds to a [`VectorDiff`] on the underlying [`ObservableVector`].
///
/// [`ObservableVector`]: eyeball_im::ObservableVector
#[derive(uniffi::Enum)]
pub enum LiveLocationShareUpdate {
Append { values: Vec<LiveLocationShare> },
Clear,
PushFront { value: LiveLocationShare },
PushBack { value: LiveLocationShare },
PopFront,
PopBack,
Insert { index: u32, value: LiveLocationShare },
Set { index: u32, value: LiveLocationShare },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<LiveLocationShare> },
}
/// Listener for live location share updates.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: SendOutsideWasm + SyncOutsideWasm + Debug {
/// Called with a batch of [`LiveLocationShareUpdate`]s whenever the list
/// of active shares changes.
fn on_update(&self, updates: Vec<LiveLocationShareUpdate>);
}
/// Tracks active live location shares in a room.
///
/// Holds the SDK [`SdkLiveLocationShares`] which keeps the beacon and
/// beacon_info event handlers registered for as long as this object is alive.
/// Call [`LiveLocationShares::subscribe`] to start receiving updates.
#[derive(uniffi::Object)]
pub struct LiveLocationShares {
inner: SdkLiveLocationShares,
}
impl LiveLocationShares {
pub fn new(inner: SdkLiveLocationShares) -> Self {
Self { inner }
}
}
#[matrix_sdk_ffi_macros::export]
impl LiveLocationShares {
/// Subscribe to changes in the list of active live location shares.
///
/// Immediately calls `listener` with a `Reset` update containing the
/// current snapshot (if non-empty), then calls it again for every
/// subsequent change that arrives from sync.
///
/// Returns a [`TaskHandle`] that, when dropped, stops the listener.
/// The event handlers remain registered for as long as this
/// [`LiveLocationShares`] object is alive.
pub fn subscribe(&self, listener: Box<dyn LiveLocationShareListener>) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe();
if !initial_values.is_empty() {
listener.on_update(vec![LiveLocationShareUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
}
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(Into::into).collect());
}
})))
}
}
impl From<SdkLiveLocationShare> for LiveLocationShare {
fn from(share: SdkLiveLocationShare) -> Self {
let start_ts = share.beacon_info.ts.0.into();
let timeout = share.beacon_info.timeout.as_millis() as u64;
let asset = share.beacon_info.asset.type_.into();
let last_location = share.last_location.map(|l| LastLocation {
location: LocationContent {
body: "".to_owned(),
geo_uri: l.location.uri.to_string(),
description: None,
zoom_level: None,
asset,
},
ts: l.ts.0.into(),
});
LiveLocationShare { user_id: share.user_id.to_string(), last_location, start_ts, timeout }
}
}
impl From<VectorDiff<SdkLiveLocationShare>> for LiveLocationShareUpdate {
fn from(diff: VectorDiff<SdkLiveLocationShare>) -> Self {
match diff {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(Into::into).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => {
Self::Insert { index: index as u32, value: value.into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: index as u32, value: value.into() }
}
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
VectorDiff::Reset { values } => {
Self::Reset { values: values.into_iter().map(Into::into).collect() }
}
}
}
}
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_ui::notification_client::{
@@ -35,6 +49,7 @@ pub struct NotificationRoomInfo {
pub topic: Option<String>,
pub join_rule: Option<JoinRule>,
pub joined_members_count: u64,
pub service_members: Vec<String>,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_space: bool,
@@ -92,6 +107,7 @@ impl NotificationItem {
topic: item.room_topic,
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
joined_members_count: item.joined_members_count,
service_members: item.service_members,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_space: item.is_space,
@@ -117,6 +133,8 @@ pub enum NotificationStatus {
/// rules, or because the user which triggered it is ignored by the
/// current user.
EventFilteredOut,
/// The event has been redacted.
EventRedacted,
}
impl From<SdkNotificationStatus> for NotificationStatus {
@@ -127,6 +145,7 @@ impl From<SdkNotificationStatus> for NotificationStatus {
}
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
SdkNotificationStatus::EventRedacted => NotificationStatus::EventRedacted,
}
}
}
@@ -1,23 +1,40 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::{Arc, RwLock};
use matrix_sdk::{
Client as MatrixClient,
event_handler::EventHandlerHandle,
notification_settings::{
NotificationSettings as SdkNotificationSettings,
RoomNotificationMode as SdkRoomNotificationMode,
},
ruma::events::push_rules::PushRulesEvent,
Client as MatrixClient,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::{
Int, RoomId, UInt,
events::push_rules::PushRulesEventContent,
push::{
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, EventMatchConditionData,
EventPropertyContainsConditionData, EventPropertyIsConditionData, HighlightTweakValue,
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition as SdkPushCondition,
RoomMemberCountConditionData, RoomMemberCountIs, RuleKind as SdkRuleKind,
ScalarJsonValue as SdkJsonValue, SenderNotificationPermissionConditionData,
Tweak as SdkTweak,
},
Int, RoomId, UInt,
};
use tokio::sync::RwLock as AsyncRwLock;
@@ -167,20 +184,22 @@ impl TryFrom<SdkPushCondition> for PushCondition {
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
Ok(match value {
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
SdkPushCondition::EventMatch(data) => {
Self::EventMatch { key: data.key, pattern: data.pattern }
}
#[allow(deprecated)]
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
SdkPushCondition::RoomMemberCount { is } => {
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
SdkPushCondition::RoomMemberCount(data) => {
Self::RoomMemberCount { prefix: data.is.prefix.into(), count: data.is.count.into() }
}
SdkPushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key: key.to_string() }
SdkPushCondition::SenderNotificationPermission(data) => {
Self::SenderNotificationPermission { key: data.key.to_string() }
}
SdkPushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
SdkPushCondition::EventPropertyIs(data) => {
Self::EventPropertyIs { key: data.key, value: data.value.into() }
}
SdkPushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
SdkPushCondition::EventPropertyContains(data) => {
Self::EventPropertyContains { key: data.key, value: data.value.into() }
}
_ => return Err("Unsupported condition type".to_owned()),
})
@@ -190,24 +209,28 @@ impl TryFrom<SdkPushCondition> for PushCondition {
impl From<PushCondition> for SdkPushCondition {
fn from(value: PushCondition) -> Self {
match value {
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
PushCondition::EventMatch { key, pattern } => {
Self::EventMatch(EventMatchConditionData::new(key, pattern))
}
#[allow(deprecated)]
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
is: RoomMemberCountIs {
PushCondition::RoomMemberCount { prefix, count } => {
Self::RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs {
prefix: prefix.into(),
count: UInt::new(count).unwrap_or_default(),
},
},
}))
}
PushCondition::SenderNotificationPermission { key } => {
Self::SenderNotificationPermission { key: key.into() }
Self::SenderNotificationPermission(SenderNotificationPermissionConditionData::new(
key.into(),
))
}
PushCondition::EventPropertyIs { key, value } => {
Self::EventPropertyIs { key, value: value.into() }
}
PushCondition::EventPropertyContains { key, value } => {
Self::EventPropertyContains { key, value: value.into() }
Self::EventPropertyIs(EventPropertyIsConditionData::new(key, value.into()))
}
PushCondition::EventPropertyContains { key, value } => Self::EventPropertyContains(
EventPropertyContainsConditionData::new(key, value.into()),
),
}
}
}
@@ -289,16 +312,19 @@ impl TryFrom<SdkTweak> for Tweak {
type Error = String;
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
Ok(match value {
SdkTweak::Sound(sound) => Self::Sound { value: sound },
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
SdkTweak::Custom { name, value } => {
let json_string = serde_json::to_string(&value)
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
Self::Custom { name, value: json_string }
Ok(match &value {
SdkTweak::Sound(sound) => Self::Sound { value: sound.to_string() },
SdkTweak::Highlight(highlight) => {
Self::Highlight { value: matches!(highlight, HighlightTweakValue::Yes) }
}
_ => {
let json_string = value
.custom_value()
.ok_or_else(|| "Unsupported tweak type".to_owned())?
.to_string();
Self::Custom { name: value.set_tweak().to_owned(), value: json_string }
}
_ => return Err("Unsupported tweak type".to_owned()),
})
}
}
@@ -308,16 +334,16 @@ impl TryFrom<Tweak> for SdkTweak {
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
Ok(match value {
Tweak::Sound { value } => Self::Sound(value),
Tweak::Highlight { value } => Self::Highlight(value),
Tweak::Custom { name, value } => {
let json_value: serde_json::Value = serde_json::from_str(&value)
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
let value = serde_json::from_value(json_value)
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
Self::Custom { name, value }
}
Tweak::Sound { value } => Self::Sound(value.into()),
Tweak::Highlight { value } => Self::Highlight(value.into()),
Tweak::Custom { name, value } => Self::new(
name,
Some(
serde_json::value::RawValue::from_string(value)
.map_err(|e| format!("Failed to convert JSON value: {e}"))?,
),
)
.map_err(|e| format!("Failed to convert custom tweak: {e}"))?,
})
}
}
@@ -0,0 +1,79 @@
use std::{error::Error, mem::MaybeUninit};
use jni::{
errors::JniError,
sys::{JNI_OK, JavaVM as RawJavaVM},
};
use tracing::debug;
static ANDROID_JVM: once_cell::sync::OnceCell<jni::JavaVM> = once_cell::sync::OnceCell::new();
/// Initialize the platform support for Android targets.
///
/// This includes setting up `rustls-platform-verifier`.
pub(crate) fn init() {
debug!("Initializing Android platform support");
ANDROID_JVM.get_or_init(|| {
match get_java_vm() {
Ok(jvm) => {
// Initialize rustls platform verifier
let mut env =
jvm.attach_current_thread_permanently().expect("Failed to attach thread");
init_rustls_platform_verifier(&mut env)
.expect("Failed to initialize rustls platform verifier");
debug!("Android platform support initialized successfully");
jvm
}
Err(e) => {
panic!("Failed to initialize Android platform support: {}", e);
}
}
});
}
fn get_java_vm() -> Result<jni::JavaVM, Box<dyn Error>> {
debug!("Getting a JVM pointer");
#[allow(non_snake_case)]
let JNI_GetCreatedJavaVMs = unsafe {
jvm_getter::find_jni_get_created_java_vms().expect("Failed to find JNI_GetCreatedJavaVMs")
};
let mut vm: MaybeUninit<*mut RawJavaVM> = MaybeUninit::uninit();
let status = unsafe { JNI_GetCreatedJavaVMs(vm.as_mut_ptr(), 1, &mut 0) };
if status != JNI_OK {
panic!("no JavaVM was found by JNI_GetCreatedJavaVMs");
}
unsafe { jni::JavaVM::from_raw(vm.assume_init()).map_err(|e| e.into()) }
}
fn init_rustls_platform_verifier(env: &mut jni::JNIEnv<'_>) -> jni::errors::Result<()> {
// Get the current activity thread
let activity_thread = env
.call_static_method(
"android/app/ActivityThread",
"currentActivityThread",
"()Landroid/app/ActivityThread;",
&[],
)?
.l()?;
// Then get the application context
let context = env
.call_method(activity_thread, "getApplication", "()Landroid/app/Application;", &[])?
.l()?;
Ok(rustls_platform_verifier::android::init_hosted(env, context)?)
}
/// Attach the current thread to a JVM one.
pub(crate) fn android_attach_current_thread_permanently()
-> jni::errors::Result<jni::JNIEnv<'static>> {
ANDROID_JVM
.get()
.ok_or_else(|| jni::errors::Error::JniCall(JniError::Unknown))?
.attach_current_thread_permanently()
}
@@ -1,31 +1,61 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::OnceLock;
#[cfg(feature = "sentry")]
use std::sync::{atomic::AtomicBool, Arc};
use std::sync::{Arc, atomic::AtomicBool};
use ::tracing::info;
#[cfg(feature = "sentry")]
use tracing::warn;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use ::tracing::warn;
use tracing_appender::rolling::Rotation;
#[cfg(feature = "sentry")]
use tracing_core::Level;
use tracing_core::Subscriber;
use tracing_subscriber::{
EnvFilter, Layer, Registry,
field::RecordFields,
fmt::{
self,
self, FormatEvent, FormatFields, FormattedFields,
format::{DefaultFields, Writer},
time::FormatTime,
FormatEvent, FormatFields, FormattedFields,
},
layer::{Layered, SubscriberExt as _},
registry::LookupSpan,
reload::{self, Handle},
util::SubscriberInitExt as _,
EnvFilter, Layer, Registry,
};
use crate::error::ClientError;
/// Default maximum total size of all log files combined (10MB).
const DEFAULT_MAX_TOTAL_SIZE_BYTES: u64 = 10 * 1024 * 1024;
/// Default maximum age of log files in seconds (1 week).
const DEFAULT_MAX_AGE_SECONDS: u64 = 7 * 24 * 60 * 60;
mod rolling_writer;
pub mod tracing;
#[cfg(target_os = "android")]
mod android_platform;
use rolling_writer::SizeAndDateRollingWriter;
#[cfg(feature = "sentry")]
use crate::tracing::BRIDGE_SPAN_NAME;
use crate::{error::ClientError, tracing::LogLevel};
use self::tracing::BRIDGE_SPAN_NAME;
use self::tracing::LogLevel;
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
@@ -119,10 +149,10 @@ where
write!(writer, "{}", span.name())?;
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
if !fields.is_empty() {
write!(writer, "{{{fields}}}")?;
}
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>()
&& !fields.is_empty()
{
write!(writer, "{{{fields}}}")?;
}
}
}
@@ -153,7 +183,7 @@ type ReloadHandle = Handle<
Layered<EnvFilter, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
SizeAndDateRollingWriter,
>,
Layered<EnvFilter, Registry>,
>;
@@ -218,21 +248,17 @@ fn make_file_layer(
Layered<EnvFilter, Registry, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
SizeAndDateRollingWriter,
> {
let mut builder = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix(&file_configuration.file_prefix);
if let Some(max_files) = file_configuration.max_files {
builder = builder.max_log_files(max_files as usize)
}
if let Some(file_suffix) = file_configuration.file_suffix {
builder = builder.filename_suffix(file_suffix)
}
let writer =
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
let writer = SizeAndDateRollingWriter::new(
&file_configuration.path,
file_configuration.file_prefix,
file_configuration.file_suffix.unwrap_or_else(|| String::from(".log")),
Rotation::HOURLY,
file_configuration.max_total_size_bytes.unwrap_or(DEFAULT_MAX_TOTAL_SIZE_BYTES),
file_configuration.max_age_seconds.unwrap_or(DEFAULT_MAX_AGE_SECONDS),
)
.expect("Failed to create a rolling file appender.");
fmt::layer()
.fmt_fields(FieldsFormatterForFiles::default())
@@ -254,13 +280,30 @@ pub struct TracingFileConfiguration {
file_prefix: String,
/// Optional suffix for the log file's names.
///
/// Default is ".log" if not specified.
file_suffix: Option<String>,
/// Maximum number of rotated files.
/// Maximum total size of all log files combined in bytes.
///
/// If not set, there's no max limit, i.e. the number of log files is
/// unlimited.
max_files: Option<u64>,
/// When the total size of all log files with the configured prefix and
/// suffix exceeds this limit, the oldest files will be removed until
/// the total is below the limit.
///
/// This is useful to prevent log files from consuming too much disk space
/// over time, even with multiple rotated files.
///
/// Default: 10MB (10 * 1024 * 1024 bytes) if not specified.
max_total_size_bytes: Option<u64>,
/// Maximum age of log files in seconds.
///
/// Log files older than this age will be automatically removed during
/// cleanup. This is checked when the writer is created and during
/// rotation operations.
///
/// Default: 1 week (7 * 24 * 60 * 60 seconds) if not specified.
max_age_seconds: Option<u64>,
}
#[derive(PartialEq, PartialOrd)]
@@ -451,9 +494,17 @@ pub struct TracingConfiguration {
/// If set, configures rotated log files where to write additional logs.
write_to_files: Option<TracingFileConfiguration>,
/// If set, the Sentry DSN to use for error reporting.
/// If set, the Sentry configuration to use for error reporting.
#[cfg(feature = "sentry")]
sentry_dsn: Option<String>,
sentry_config: Option<SentryConfig>,
}
#[cfg(feature = "sentry")]
#[derive(uniffi::Record)]
pub struct SentryConfig {
dsn: String,
app_version: String,
app_platform: String,
}
impl TracingConfiguration {
@@ -462,7 +513,12 @@ impl TracingConfiguration {
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
fn build(mut self) -> LoggingCtx {
// Show full backtraces, if we run into panics.
std::env::set_var("RUST_BACKTRACE", "1");
//
// FIXME: Use safe API for this once stable. Tracking issue:
// https://github.com/rust-lang/rust/issues/93346
unsafe {
std::env::set_var("RUST_BACKTRACE", "1");
}
// Log panics.
log_panics::init();
@@ -474,18 +530,14 @@ impl TracingConfiguration {
{
// Prepare the Sentry layer, if a DSN is provided.
let (sentry_layer, sentry_logging_ctx) =
if let Some(sentry_dsn) = self.sentry_dsn.take() {
if let Some(sentry_config) = self.sentry_config.take() {
// Initialize the Sentry client with the given options.
let sentry_guard = sentry::init((
sentry_dsn,
sentry_config.dsn,
sentry::ClientOptions {
traces_sampler: Some(Arc::new(|ctx| {
// Make sure bridge spans are always uploaded
if ctx.name() == BRIDGE_SPAN_NAME {
1.0
} else {
0.0
}
if ctx.name() == BRIDGE_SPAN_NAME { 1.0 } else { 0.0 }
})),
attach_stacktrace: true,
release: Some(env!("VERGEN_GIT_SHA").into()),
@@ -493,6 +545,11 @@ impl TracingConfiguration {
},
));
sentry::configure_scope(|scope| {
scope.set_tag("app_version", sentry_config.app_version);
scope.set_tag("app_platform", sentry_config.app_platform);
});
let sentry_enabled = Arc::new(AtomicBool::new(true));
// Add a Sentry layer to the tracing subscriber.
@@ -558,7 +615,7 @@ impl TracingConfiguration {
}
// Log the log levels 🧠.
tracing::info!(env_filter, "Logging has been set up");
info!(env_filter, "Logging has been set up");
logging_ctx
}
@@ -628,6 +685,9 @@ pub fn init_platform(
}
}
#[cfg(target_os = "android")]
android_platform::init();
Ok(())
}
@@ -685,6 +745,10 @@ fn setup_multithreaded_tokio_runtime() {
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
#[cfg(target_os = "android")]
builder.on_thread_start(|| {
_ = android_platform::android_attach_current_thread_permanently();
});
builder
}));
}
@@ -735,7 +799,7 @@ mod tests {
write_to_stdout_or_system: true,
write_to_files: None,
#[cfg(feature = "sentry")]
sentry_dsn: None,
sentry_config: None,
};
let filter = build_tracing_filter(&config);
@@ -781,7 +845,7 @@ mod tests {
write_to_stdout_or_system: true,
write_to_files: None,
#[cfg(feature = "sentry")]
sentry_dsn: None,
sentry_config: None,
};
let filter = build_tracing_filter(&config);
@@ -828,7 +892,7 @@ mod tests {
write_to_stdout_or_system: true,
write_to_files: None,
#[cfg(feature = "sentry")]
sentry_dsn: None,
sentry_config: None,
};
let filter = build_tracing_filter(&config);
@@ -0,0 +1,914 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{
fs::{self, File, OpenOptions},
io::{self, Write},
path::{Path, PathBuf},
sync::Mutex,
time::SystemTime,
};
use tracing_appender::rolling::Rotation;
/// Custom rolling file appender that supports time-based rotation and
/// size-based cleanup.
///
/// This writer automatically manages log files with the following behavior:
///
/// # File Naming
///
/// Log files are named using the pattern: `{prefix}.{timestamp}.{suffix}`
/// where the timestamp format depends on the rotation period:
/// - `MINUTELY`: `YYYY-MM-DD-HH-MM`
/// - `HOURLY`: `YYYY-MM-DD-HH`
/// - `DAILY`: `YYYY-MM-DD`
/// - `WEEKLY` or `NEVER`: `YYYY-Www` (ISO week number, e.g., `2024-W03`)
///
/// # Automatic Rotation
///
/// Files are rotated (a new file is created) when the configured time period
/// changes. For example, with hourly rotation, a new file is created when the
/// hour changes. Rotation is checked:
/// - During writer initialization (creates/opens file for current period)
/// - Before each write operation (only rotates if time period has changed)
///
/// If a log file already exists for the current time period, it will be
/// reopened and appended to rather than creating a new file.
///
/// # Automatic Cleanup
///
/// The writer performs cleanup operations during initialization and rotation:
/// - **Size limit enforcement**: When total size of all log files exceeds
/// `max_total_size_bytes`, the oldest files are removed until under the limit
/// - **Age-based cleanup**: Files older than `max_age_seconds` (based on
/// filesystem modification time) are automatically removed
/// - **File filtering**: Only files matching both the configured prefix and
/// suffix are managed; other files in the directory are left untouched
///
/// # Side Effects on Creation
///
/// When `new()` is called, the following side effects occur:
/// 1. Creates the log directory if it doesn't exist (including parent
/// directories)
/// 2. Creates or opens a log file for the current time period (appends if
/// exists)
/// 3. Performs cleanup of old files based on age (by filesystem mtime)
/// 4. Enforces the total size limit by removing oldest files if needed
///
/// # Thread Safety
///
/// This writer is safe to use from multiple threads. Internal state is
/// protected by a mutex, ensuring that file operations and rotations are
/// properly synchronized.
pub(super) struct SizeAndDateRollingWriter {
config: WriterConfig,
state: Mutex<Option<WriterState>>,
}
/// Immutable configuration for the writer - shared without locks.
///
/// This struct contains all configuration parameters that remain constant
/// throughout the writer's lifetime. Since these values never change, they
/// can be safely shared across threads without synchronization.
struct WriterConfig {
/// Directory where log files are created
base_path: PathBuf,
/// Prefix for log file names (e.g., "app" results in "app.2024-01-15.log")
file_prefix: String,
/// Suffix for log file names (typically ".log")
file_suffix: String,
/// Time period for automatic rotation (MINUTELY, HOURLY, DAILY, WEEKLY, or
/// NEVER which is treated as WEEKLY)
rotation: Rotation,
/// Maximum total size in bytes of all log files before cleanup
max_total_size_bytes: u64,
/// Maximum age in seconds before a log file is removed during cleanup
max_age_seconds: u64,
}
/// Mutable state that requires synchronization.
///
/// This struct contains the current log file handle and its path. These values
/// change when rotation occurs, so access must be protected by a mutex to
/// ensure thread safety.
///
/// The state is wrapped in `Option` because it needs to be temporarily taken
/// during rotation operations to allow mutation while holding the lock.
struct WriterState {
/// The currently open log file handle for writing
current_file: File,
/// Path to the current log file (used to identify it during cleanup)
current_path: PathBuf,
}
impl SizeAndDateRollingWriter {
/// Creates a new rolling writer with the specified configuration.
///
/// # Arguments
///
/// * `path` - Directory where log files will be created. Will be created if
/// it doesn't exist.
/// * `file_prefix` - Prefix for log file names (e.g., "app")
/// * `file_suffix` - Suffix for log file names (e.g., ".log")
/// * `rotation` - Time period for rotation (MINUTELY, HOURLY, DAILY,
/// WEEKLY, or NEVER which is treated as WEEKLY)
/// * `max_total_size_bytes` - Maximum total size of all log files. When
/// exceeded, oldest files are removed.
/// * `max_age_seconds` - Maximum age of log files in seconds. Files older
/// than this (by filesystem mtime) are removed during cleanup.
///
/// # Side Effects
///
/// This method performs several file system operations in order:
/// 1. Creates the directory at `path` if it doesn't exist
/// 2. Creates or reopens a log file for the current time period (appends if
/// exists)
/// 3. Scans the directory for existing log files matching the prefix/suffix
/// 4. Removes files older than `max_age_seconds` (by filesystem mtime)
/// 5. Removes oldest files if total size exceeds `max_total_size_bytes`
///
/// # Errors
///
/// Returns an error if:
/// - The directory cannot be created
/// - The directory cannot be read
/// - The log file cannot be created or opened
/// - File metadata cannot be read during cleanup
///
/// # Examples
///
/// ```ignore
/// let writer = SizeAndDateRollingWriter::new(
/// "/var/log/myapp",
/// "app".to_owned(),
/// ".log".to_owned(),
/// Rotation::HOURLY,
/// 100 * 1024 * 1024, // 100 MB
/// 7 * 24 * 60 * 60, // 7 days
/// )?;
/// ```
pub(super) fn new(
path: impl AsRef<Path>,
file_prefix: String,
file_suffix: String,
rotation: Rotation,
max_total_size_bytes: u64,
max_age_seconds: u64,
) -> io::Result<Self> {
let base_path = path.as_ref().to_path_buf();
fs::create_dir_all(&base_path)?;
let config = WriterConfig {
base_path,
file_prefix,
file_suffix,
rotation,
max_total_size_bytes,
max_age_seconds,
};
// Create initial state with first rotation
let mut state = None;
Self::rotate_internal(&config, &mut state, false)?;
Ok(Self { config, state: Mutex::new(state) })
}
/// Extract the timestamp from the current filename.
fn extract_timestamp_from_path(config: &WriterConfig, current_path: &Path) -> Option<String> {
let filename = current_path.file_name()?.to_str()?;
// Strip prefix and suffix to get the timestamp
// Format: "prefix.timestamp.suffix"
let without_prefix = filename.strip_prefix(&format!("{}.", config.file_prefix))?;
let timestamp = without_prefix.strip_suffix(&config.file_suffix)?;
Some(timestamp.to_owned())
}
/// Check if rotation is needed based on time period change.
fn should_rotate_by_time(config: &WriterConfig, current_path: &Path) -> bool {
let current_time = Self::format_rotation_timestamp(config);
let last_rotation_time = Self::extract_timestamp_from_path(config, current_path);
// If we can't extract the timestamp, assume rotation is needed
match last_rotation_time {
Some(last_time) => current_time != last_time,
None => true,
}
}
/// Format the current time as a timestamp string for the rotation period.
///
/// Returns a timestamp string based on the configured rotation period
/// (e.g., "2024-01-15-14" for hourly rotation).
fn format_rotation_timestamp(config: &WriterConfig) -> String {
let now = chrono::Local::now();
match config.rotation {
Rotation::MINUTELY => now.format("%Y-%m-%d-%H-%M").to_string(),
Rotation::HOURLY => now.format("%Y-%m-%d-%H").to_string(),
Rotation::DAILY => now.format("%Y-%m-%d").to_string(),
Rotation::WEEKLY | Rotation::NEVER => now.format("%Y-W%W").to_string(),
}
}
/// Rotate the log file, creating a new file with a timestamp-based name.
///
/// If `check_conditions` is true, rotation only happens if time or size
/// thresholds are met. Otherwise, rotation is forced.
///
/// This method also handles initial state creation when called with None
/// state.
fn rotate_internal(
config: &WriterConfig,
state: &mut Option<WriterState>,
check_conditions: bool,
) -> io::Result<()> {
// Check if rotation is needed (skip for uninitialized state)
if check_conditions
&& let Some(state) = state.as_ref()
&& !Self::should_rotate_by_time(config, &state.current_path)
{
return Ok(());
}
let time_str = Self::format_rotation_timestamp(config);
// Generate filename with timestamp
let filename = format!("{}.{}{}", config.file_prefix, time_str, config.file_suffix);
let new_path = config.base_path.join(filename);
// Open or create file in append mode
let new_file = OpenOptions::new().create(true).append(true).open(&new_path)?;
let new_state = WriterState { current_file: new_file, current_path: new_path };
// Clean up logs older than configured max age
Self::trim_old_logs_internal(config, &new_state)?;
// Enforce total size limit by removing oldest files if needed
Self::enforce_total_size_limit_internal(config, &new_state)?;
*state = Some(new_state);
Ok(())
}
/// Get all log files matching our prefix and suffix, sorted by modification
/// time.
///
/// Returns a vector of (path, modification_time) tuples, oldest first.
fn get_matching_log_files(config: &WriterConfig) -> io::Result<Vec<(PathBuf, SystemTime)>> {
let mut files: Vec<_> = fs::read_dir(&config.base_path)?
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() {
let filename = path.file_name()?.to_str()?;
// Only process files matching our prefix and suffix
if filename.starts_with(&config.file_prefix)
&& filename.ends_with(&config.file_suffix)
{
let metadata = fs::metadata(&path).ok()?;
let modified = metadata.modified().ok()?;
return Some((path, modified));
}
}
None
})
.collect();
// Sort by modification time (oldest first)
files.sort_by_key(|(_, modified)| *modified);
Ok(files)
}
/// Remove all log files older than the configured max age.
///
/// Only files matching the configured prefix and suffix are removed.
/// Other files in the directory are left alone.
fn trim_old_logs_internal(config: &WriterConfig, state: &WriterState) -> io::Result<()> {
let now = SystemTime::now();
let files = Self::get_matching_log_files(config)?;
for (path, modified) in files {
// Skip the current file
if path == state.current_path {
continue;
}
// Check if file is older than max age
if let Ok(duration) = now.duration_since(modified)
&& duration.as_secs() > config.max_age_seconds
{
let _ = fs::remove_file(path);
}
}
Ok(())
}
/// Enforce total size limit across all log files.
///
/// If the total size of all matching log files exceeds
/// max_total_size_bytes, remove the oldest files until the total is
/// below the limit.
fn enforce_total_size_limit_internal(
config: &WriterConfig,
state: &WriterState,
) -> io::Result<()> {
let max_total = config.max_total_size_bytes;
let files = Self::get_matching_log_files(config)?;
// Calculate total size of all log files
let mut total_size: u64 = 0;
for (path, _) in &files {
if let Ok(metadata) = fs::metadata(path) {
total_size += metadata.len();
}
}
// If under limit, nothing to do
if total_size <= max_total {
return Ok(());
}
// Remove oldest files until we're under the limit
// Files are already sorted by modification time (oldest first)
for (path, _) in files {
// Don't remove the current file
if path == state.current_path {
continue;
}
if let Ok(metadata) = fs::metadata(&path) {
let file_size = metadata.len();
let _ = fs::remove_file(&path);
total_size = total_size.saturating_sub(file_size);
// Check if we're now under the limit
if total_size <= max_total {
break;
}
}
}
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SizeAndDateRollingWriter {
type Writer = SizeAndDateRollingWriterHandle<'a>;
fn make_writer(&'a self) -> Self::Writer {
SizeAndDateRollingWriterHandle {
config: &self.config,
state: &self.state,
_phantom: std::marker::PhantomData,
}
}
}
pub(super) struct SizeAndDateRollingWriterHandle<'a> {
config: &'a WriterConfig,
state: &'a Mutex<Option<WriterState>>,
_phantom: std::marker::PhantomData<&'a ()>,
}
impl<'a> Write for SizeAndDateRollingWriterHandle<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut state = self.state.lock().unwrap();
// Check if rotation is needed
SizeAndDateRollingWriter::rotate_internal(self.config, &mut state, true)?;
// Write to file (state must be initialized after rotation)
state.as_mut().unwrap().current_file.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
let mut state = self.state.lock().unwrap();
if let Some(s) = state.as_mut() { s.current_file.flush() } else { Ok(()) }
}
}
#[cfg(test)]
impl SizeAndDateRollingWriter {
/// Manually trigger log rotation if conditions are met.
///
/// This will rotate the current log file if the time period has changed.
fn roll(&self) -> io::Result<()> {
let mut state = self.state.lock().unwrap();
Self::rotate_internal(&self.config, &mut state, true)
}
/// Manually trigger cleanup of old log files.
///
/// This removes all log files older than the configured max age, keeping
/// only files that match the configured prefix and suffix.
fn trim(&self) -> io::Result<()> {
let state = self.state.lock().unwrap();
if let Some(ref state) = *state {
Self::trim_old_logs_internal(&self.config, state)
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::tempdir;
use tracing_subscriber::fmt::MakeWriter;
use super::*;
#[test]
fn test_rotation_file_naming() {
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
let _writer = SizeAndDateRollingWriter::new(
log_path,
"app".to_owned(),
".log".to_owned(),
Rotation::HOURLY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// Check file naming pattern
let log_files: Vec<_> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() {
let filename = path.file_name()?.to_str()?.to_owned();
if filename.starts_with("app") && filename.ends_with(".log") {
Some(filename)
} else {
None
}
} else {
None
}
})
.collect();
// Should have one file with proper naming
assert_eq!(log_files.len(), 1, "Expected exactly one log file");
// Check that files follow the expected naming pattern
for filename in &log_files {
assert!(
filename.starts_with("app."),
"Filename should start with prefix: {}",
filename
);
assert!(filename.ends_with(".log"), "Filename should end with suffix: {}", filename);
}
}
#[test]
fn test_prefix_suffix_filtering() {
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
// Create some unrelated files
std::fs::write(log_path.join("other.txt"), "unrelated").unwrap();
std::fs::write(log_path.join("app.txt"), "wrong suffix").unwrap();
std::fs::write(log_path.join("test.log"), "wrong prefix").unwrap();
let writer = SizeAndDateRollingWriter::new(
log_path,
"app".to_owned(),
".log".to_owned(),
Rotation::HOURLY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
let mut handle = writer.make_writer();
handle.write_all(b"test message\n").unwrap();
handle.flush().unwrap();
// Trigger manual trim
writer.trim().unwrap();
// Check that unrelated files still exist
assert!(log_path.join("other.txt").exists(), "Unrelated file should not be removed");
assert!(log_path.join("app.txt").exists(), "File with wrong suffix should not be removed");
assert!(log_path.join("test.log").exists(), "File with wrong prefix should not be removed");
// App log file should still exist
let app_logs: Vec<_> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let filename = entry.file_name().to_str()?.to_owned();
if filename.starts_with("app.") && filename.ends_with(".log") {
Some(filename)
} else {
None
}
})
.collect();
assert_eq!(app_logs.len(), 1, "Expected one app log file");
}
#[test]
fn test_manual_roll_and_trim() {
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
let writer = SizeAndDateRollingWriter::new(
log_path,
"manual".to_owned(),
".log".to_owned(),
Rotation::DAILY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// Manual roll should work
assert!(writer.roll().is_ok(), "Manual roll should succeed");
// Manual trim should work
assert!(writer.trim().is_ok(), "Manual trim should succeed");
// Should still have log file
let log_files: Vec<_> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("manual") {
Some(path)
} else {
None
}
})
.collect();
assert!(!log_files.is_empty(), "Should have at least one log file");
}
#[test]
fn test_total_size_limit_removes_oldest_files() {
// This test verifies that when the total size of all log files exceeds
// max_total_size_bytes, the oldest files are removed
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
// Manually create several log files with different timestamps (alphabetically
// sorted = oldest first) Total of 240 bytes
std::fs::write(log_path.join("total.2024-01-01-10-00.log"), "x".repeat(80)).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
std::fs::write(log_path.join("total.2024-01-01-10-01.log"), "y".repeat(80)).unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
std::fs::write(log_path.join("total.2024-01-01-10-02.log"), "z".repeat(80)).unwrap();
let count_files = || {
std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
Some(path)
} else {
None
}
})
.count()
};
assert_eq!(count_files(), 3, "Should have 3 log files");
// Now create a new writer with 200 byte total limit
// Current total is 240 bytes. The writer will:
// 1. Create a new file with current timestamp
// 2. See total exceeds 200 bytes
// 3. Remove oldest file(s) until under limit
let _writer = SizeAndDateRollingWriter::new(
log_path,
"total".to_owned(),
".log".to_owned(),
Rotation::MINUTELY,
200, // 200 byte total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// After writer creation, we should still have 3 files:
// - Oldest file (10-00) was removed
// - Two middle files (10-01, 10-02) remain
// - New current file was created
let remaining_files = count_files();
assert_eq!(
remaining_files, 3,
"Should have 3 files: 2 old files + 1 new current file, but have {}",
remaining_files
);
// Calculate total size of remaining files
let total_size: u64 = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
Some(std::fs::metadata(&path).ok()?.len())
} else {
None
}
})
.sum();
// Total should be 160 bytes (80 + 80 + 0 for new file)
assert!(total_size <= 200, "Total size should be under 200 bytes, but is {}", total_size);
// Verify the oldest file was removed by checking filenames
let filenames: Vec<String> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() { path.file_name()?.to_str().map(|s| s.to_owned()) } else { None }
})
.collect();
assert!(
!filenames.contains(&"total.2024-01-01-10-00.log".to_owned()),
"Oldest file should have been removed"
);
assert!(
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-01")),
"Second file should still exist"
);
assert!(
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-02")),
"Third file should still exist"
);
}
#[test]
fn test_time_based_rotation_logic() {
// Test that the rotation logic correctly identifies when rotation is needed
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
let writer = SizeAndDateRollingWriter::new(
log_path,
"time".to_owned(),
".log".to_owned(),
Rotation::DAILY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// Write some data
let mut handle = writer.make_writer();
handle.write_all(b"initial log entry\n").unwrap();
handle.flush().unwrap();
// Verify initial state - should have created one file
let initial_files: Vec<_> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("time") {
Some(path.file_name()?.to_str()?.to_owned())
} else {
None
}
})
.collect();
assert_eq!(initial_files.len(), 1, "Should have one log file initially");
// Verify the file contains our data
let file_content = std::fs::read_to_string(log_path.join(&initial_files[0])).unwrap();
assert!(file_content.contains("initial log entry"));
}
#[test]
fn test_old_files_cleanup() {
// Test that files older than one week are removed
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
// Create files with timestamps more than a week old
// We can't easily manipulate file mtimes without external crates,
// but we can verify the cleanup logic doesn't fail
std::fs::write(log_path.join("old.2020-01-01-10-00.log"), "old data").unwrap();
// Create a writer which will trigger cleanup
let writer = SizeAndDateRollingWriter::new(
log_path,
"old".to_owned(),
".log".to_owned(),
Rotation::HOURLY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// The old file should still exist because we can't manipulate mtime easily
// But we verify that cleanup doesn't crash
writer.trim().unwrap();
// Verify we can still write
let mut handle = writer.make_writer();
handle.write_all(b"new data").unwrap();
handle.flush().unwrap();
}
#[test]
fn test_write_operations() {
// Test that writing works correctly across multiple writes
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
let writer = SizeAndDateRollingWriter::new(
log_path,
"write".to_owned(),
".log".to_owned(),
Rotation::HOURLY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// Write multiple times
for i in 0..5 {
let mut handle = writer.make_writer();
let message = format!("Log entry {}\n", i);
handle.write_all(message.as_bytes()).unwrap();
handle.flush().unwrap();
}
// Verify all writes went to the same file (same hour)
let log_files: Vec<_> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("write") {
Some(path)
} else {
None
}
})
.collect();
assert_eq!(log_files.len(), 1, "Should have one log file for same time period");
// Verify content
let content = std::fs::read_to_string(&log_files[0]).unwrap();
for i in 0..5 {
assert!(content.contains(&format!("Log entry {}", i)));
}
}
#[test]
fn test_reopening_existing_file() {
// Test that reopening appends to existing file within same time period
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
// Create first writer and write
let writer1 = SizeAndDateRollingWriter::new(
log_path,
"reopen".to_owned(),
".log".to_owned(),
Rotation::DAILY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
let mut handle1 = writer1.make_writer();
handle1.write_all(b"first write\n").unwrap();
handle1.flush().unwrap();
drop(writer1);
// Create second writer (simulating restart within same day)
let writer2 = SizeAndDateRollingWriter::new(
log_path,
"reopen".to_owned(),
".log".to_owned(),
Rotation::DAILY,
10 * 1024 * 1024, // 10MB total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
let mut handle2 = writer2.make_writer();
handle2.write_all(b"second write\n").unwrap();
handle2.flush().unwrap();
// Should still have only one file
let log_files: Vec<_> = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("reopen") {
Some(path)
} else {
None
}
})
.collect();
assert_eq!(log_files.len(), 1, "Should have one log file");
// Verify both writes are in the file
let content = std::fs::read_to_string(&log_files[0]).unwrap();
assert!(content.contains("first write"));
assert!(content.contains("second write"));
}
#[test]
fn test_total_size_limit_with_multiple_old_files() {
// Test that multiple old files are removed until under limit
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path();
// Create 5 files, each 50 bytes (total 250 bytes)
for i in 0..5 {
let filename = format!("multi.2024-01-01-10-0{}.log", i);
std::fs::write(log_path.join(filename), "x".repeat(50)).unwrap();
}
let count_files = || {
std::fs::read_dir(log_path)
.unwrap()
.filter(|entry| {
if let Ok(entry) = entry
&& let Some(name) = entry.file_name().to_str()
{
return name.starts_with("multi");
}
false
})
.count()
};
assert_eq!(count_files(), 5, "Should start with 5 files");
// Create writer with 100 byte limit (should keep only 2 old files + new
// current)
let _writer = SizeAndDateRollingWriter::new(
log_path,
"multi".to_owned(),
".log".to_owned(),
Rotation::MINUTELY,
100, // 100 byte total size limit
7 * 24 * 60 * 60, // 1 week in seconds
)
.unwrap();
// Should have removed oldest files
let remaining = count_files();
assert!(remaining <= 3, "Should have removed multiple old files to stay under limit");
// Verify total size is under limit
let total_size: u64 = std::fs::read_dir(log_path)
.unwrap()
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.is_file() && path.file_name()?.to_str()?.starts_with("multi") {
Some(std::fs::metadata(&path).ok()?.len())
} else {
None
}
})
.sum();
assert!(total_size <= 100, "Total size should be under 100 bytes, but is {}", total_size);
}
}
@@ -1,12 +1,25 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
#[cfg(feature = "sentry")]
use std::borrow::ToOwned;
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
sync::{Arc, Mutex, OnceLock},
};
use once_cell::sync::OnceCell;
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
use tracing::{Callsite, callsite::DefaultCallsite, debug, error, field::FieldSet};
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// Log an event.
@@ -54,7 +67,7 @@ fn get_or_init_metadata(
meta_kind: MetadataKind,
) -> &'static DefaultCallsite {
mutex.lock().unwrap().entry(id).or_insert_with_key(|id| {
let callsite = Box::leak(Box::new(LateInitCallsite(OnceCell::new())));
let callsite = Box::leak(Box::new(LateInitCallsite(OnceLock::new())));
let metadata = Box::leak(Box::new(tracing::Metadata::new(
Box::leak(
id.name
@@ -73,7 +86,7 @@ fn get_or_init_metadata(
FieldSet::new(field_names, identify_callsite!(callsite)),
meta_kind,
)));
callsite.0.try_insert(DefaultCallsite::new(metadata)).expect("callsite was not set before")
callsite.0.get_or_init(|| DefaultCallsite::new(metadata))
})
}
@@ -254,7 +267,7 @@ struct MetadataId {
name: Option<String>,
}
struct LateInitCallsite(OnceCell<DefaultCallsite>);
struct LateInitCallsite(OnceLock<DefaultCallsite>);
impl Callsite for LateInitCallsite {
fn set_interest(&self, interest: tracing_core::Interest) {
+91 -11
View File
@@ -1,14 +1,28 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use matrix_sdk::authentication::oauth::{
OAuth,
qrcode::{
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
},
OAuth,
};
use matrix_sdk_base::crypto::types::qr_login;
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt};
use crate::{
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
@@ -259,6 +273,26 @@ impl QrCodeData {
qr_login::QrCodeIntentData::Msc4388 { .. } => None,
}
}
/// The base URL of the homeserver contained within the scanned QR code
/// data.
///
/// Note: This value is only present when scanning a QR code conforming to
/// MSC4388.
pub fn base_url(&self) -> Option<String> {
match self.inner.intent_data() {
qrcode::QrCodeIntentData::Msc4108 { .. } => None,
qrcode::QrCodeIntentData::Msc4388 { base_url, .. } => Some(base_url.to_string()),
}
}
/// Get the [`QrCodeIntent`] of this [`QrCodeData`] object.
///
/// This tells us if the creator of the QR code wants to log in or if they
/// want to log another device in.
pub fn intent(&self) -> QrCodeIntent {
self.inner.intent()
}
}
/// Error type for the decoding of the [`QrCodeData`].
@@ -298,6 +332,8 @@ pub enum HumanQrLoginError {
CheckCodeCannotBeSent,
#[error("The rendezvous session was not found and might have expired")]
NotFound,
#[error("The QR code specifies an unsupported protocol version")]
UnsupportedQrCodeType,
}
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
@@ -328,8 +364,10 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_)
| SecureChannelError::UnsupportedQrCodeType => HumanQrLoginError::Unknown,
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::UnsupportedQrCodeType => {
HumanQrLoginError::UnsupportedQrCodeType
}
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode
@@ -384,23 +422,44 @@ pub enum HumanQrGrantLoginError {
#[error("The rendezvous session was not found and might have expired")]
NotFound,
/// The device could not be created.
#[error("The device could not be created.")]
UnableToCreateDevice,
/// An unknown error has happened.
#[error("An unknown error has happened.")]
Unknown(String),
/// The requested device was not returned by the homeserver.
#[error("The requested device was not returned by the homeserver.")]
DeviceNotFound,
/// The other device is already signed in and so does not need to sign in.
#[error("The other device is already signed and so does not need to sign in.")]
OtherDeviceAlreadySignedIn,
/// The sign in was cancelled.
#[error("The sign in was cancelled.")]
Cancelled,
/// The sign in was not completed in the required time.
#[error("The sign in was not completed in the required time.")]
Expired,
/// A secure connection could not have been established between the two
/// devices.
#[error("A secure connection could not have been established between the two devices.")]
ConnectionInsecure,
/// The QR code specifies an unsupported protocol version.
#[error("The QR code specifies an unsupported protocol version")]
UnsupportedQrCodeType,
}
impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
fn from(value: qrcode::QRCodeGrantLoginError) -> Self {
use qrcode::QRCodeGrantLoginError;
use qrcode::{QRCodeGrantLoginError, SecureChannelError};
match value {
QRCodeGrantLoginError::DeviceIDAlreadyInUse => Self::DeviceIDAlreadyInUse,
QRCodeGrantLoginError::DeviceNotFound => Self::DeviceNotFound,
QRCodeGrantLoginError::InvalidCheckCode => Self::InvalidCheckCode,
QRCodeGrantLoginError::UnableToCreateDevice => Self::UnableToCreateDevice,
QRCodeGrantLoginError::UnsupportedProtocol(protocol) => {
Self::UnsupportedProtocol(protocol.to_string())
}
@@ -408,7 +467,28 @@ impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
Self::MissingSecretsBackup(error.map_or("other".to_owned(), |e| e.to_string()))
}
QRCodeGrantLoginError::NotFound => Self::NotFound,
QRCodeGrantLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => Self::Unknown(e.to_string()),
SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode
| SecureChannelError::CannotReceiveCheckCode => Self::ConnectionInsecure,
SecureChannelError::InvalidIntent => Self::OtherDeviceAlreadySignedIn,
},
QRCodeGrantLoginError::UnexpectedMessage { .. } => Self::Unknown(value.to_string()),
QRCodeGrantLoginError::Unknown(string) => Self::Unknown(string),
QRCodeGrantLoginError::LoginFailure { reason, .. } => match reason {
LoginFailureReason::UnsupportedProtocol => Self::UnsupportedProtocol(
"Other device does not support any of our protocols".to_owned(),
),
LoginFailureReason::AuthorizationExpired => Self::Expired,
LoginFailureReason::UserCancelled => Self::Cancelled,
_ => Self::Unknown(reason.to_string()),
},
}
}
}
+119 -216
View File
@@ -1,64 +1,75 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use futures_util::{pin_mut, StreamExt};
use futures_util::{StreamExt, pin_mut};
use matrix_sdk::{
encryption::LocalTrust,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges,
ListThreadsOptions as SdkListThreadsOptions, Room as SdkRoom, RoomMemberRole,
TryFromReportedContentScoreError,
},
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
SuccessorRoom as SdkSuccessorRoom,
encryption::LocalTrust,
room::{
Room as SdkRoom, RoomMemberRole, edit::EditedContent, power_levels::RoomPowerLevelChanges,
},
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
timeline::{default_event_filter, RoomExt, TimelineBuilder},
timeline::{RoomExt, TimelineBuilder, default_event_filter},
unable_to_decrypt_hook::UtdHookManager,
};
use mime::Mime;
use ruma::{
api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads,
assign,
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
ServerName, UserId, assign,
events::{
AnyMessageLikeEventContent, AnySyncTimelineEvent,
receipt::ReceiptThread,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
MediaSource as RumaMediaSource, avatar::ImageInfo as RumaAvatarImageInfo,
history_visibility::HistoryVisibility as RumaHistoryVisibility,
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
MediaSource as RumaMediaSource,
},
AnyMessageLikeEventContent, AnySyncTimelineEvent,
},
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
ServerName, UserId,
};
use tracing::{error, warn};
use tracing::error;
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
use crate::{
TaskHandle,
chunk_iterator::ChunkIterator,
client::{JoinRule, RoomVisibility},
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
error::{
ClientError, LiveLocationError, MediaInfoError, NotYetImplemented, QueueWedgeError,
RoomError,
},
event::TimelineEvent,
identity_status_change::IdentityStatusChange,
live_location_share::{LastLocation, LiveLocationShare},
live_location_share::LiveLocationShares,
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
},
ruma::{AudioInfo, FileInfo, ImageInfo, MediaSource, ThumbnailInfo, VideoInfo},
runtime::get_runtime_handle,
timeline::{
configuration::{TimelineConfiguration, TimelineFilter},
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
configuration::{TimelineConfiguration, TimelineFilter},
threads::{ThreadListService, ThreadSubscription},
},
utils::{u64_to_uint, AsyncRuntimeDropped},
TaskHandle,
utils::{AsyncRuntimeDropped, u64_to_uint},
};
mod power_levels;
@@ -427,6 +438,37 @@ impl Room {
Ok(())
}
/// Send a raw state event to the room.
///
/// # Arguments
///
/// * `event_type` - The type of the state event to send (e.g.
/// `"m.room.name"` or a custom type).
///
/// * `state_key` - A unique key which defines the overwriting semantics for
/// this piece of room state. This is often an empty string.
///
/// * `content` - The content of the state event encoded as a JSON string.
///
/// Returns the event ID of the newly created state event.
pub async fn send_state_event_raw(
&self,
event_type: String,
state_key: String,
content: String,
) -> Result<String, ClientError> {
let content_json: serde_json::Value =
serde_json::from_str(&content).map_err(|e| ClientError::Generic {
msg: format!("Failed to parse JSON: {e}"),
details: Some(format!("{e:?}")),
})?;
let response =
self.inner.send_state_event_raw(&event_type, &state_key, content_json).await?;
Ok(response.event_id.to_string())
}
/// Redacts an event from the room.
///
/// # Arguments
@@ -470,18 +512,9 @@ impl Room {
pub async fn report_content(
&self,
event_id: String,
score: Option<i32>,
reason: Option<String>,
) -> Result<(), ClientError> {
self.inner
.report_content(
EventId::parse(event_id)?,
score.map(TryFrom::try_from).transpose().map_err(
|error: TryFromReportedContentScoreError| ClientError::from_err(error),
)?,
reason,
)
.await?;
self.inner.report_content(EventId::parse(event_id)?, reason).await?;
Ok(())
}
@@ -1054,17 +1087,14 @@ impl Room {
}
/// Stop the current users live location share in the room.
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
pub async fn stop_live_location_share(&self) -> Result<(), LiveLocationError> {
self.inner.stop_live_location_share().await?;
Ok(())
}
/// Send the current users live location beacon in the room.
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
self.inner
.send_location_beacon(geo_uri)
.await
.expect("Unable to send live location beacon");
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), LiveLocationError> {
self.inner.send_location_beacon(geo_uri).await?;
Ok(())
}
@@ -1106,46 +1136,16 @@ impl Room {
}))))
}
/// Subscribes to live location shares in this room, using a `listener` to
/// be notified of the changes.
/// Returns the active live location shares for this room.
///
/// The current live location shares will be emitted immediately when
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
pub fn subscribe_to_live_location_shares(
self: Arc<Self>,
listener: Box<dyn LiveLocationShareListener>,
) -> Arc<TaskHandle> {
let room = self.inner.clone();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let subscription = room.observe_live_location_shares();
let stream = subscription.subscribe();
let mut pinned_stream = pin!(stream);
while let Some(event) = pinned_stream.next().await {
let last_location = LocationContent {
body: "".to_owned(),
geo_uri: event.last_location.location.uri.clone().to_string(),
description: None,
zoom_level: None,
asset: None,
};
let Some(beacon_info) = event.beacon_info else {
warn!("Live location share is missing the associated beacon_info state, skipping event.");
continue;
};
listener.call(vec![LiveLocationShare {
last_location: LastLocation {
location: last_location,
ts: event.last_location.ts.0.into(),
},
is_live: beacon_info.is_live(),
user_id: event.user_id.to_string(),
}])
}
})))
/// The returned [`LiveLocationShares`] object tracks which users are
/// currently sharing their live location. It keeps the underlying event
/// handlers registered — and therefore the share list up-to-date — for as
/// long as it is alive. Call [`LiveLocationShares::subscribe`] on it to
/// receive an initial snapshot and a stream of incremental updates.
pub async fn live_location_shares(&self) -> Arc<LiveLocationShares> {
let inner = self.inner.live_location_shares().await;
Arc::new(LiveLocationShares::new(inner))
}
/// Forget this room.
@@ -1180,12 +1180,11 @@ impl Room {
// If no server names are provided and the room's membership is invited,
// add the server name from the sender's user id as a fallback value
if server_names.is_empty() {
if let Ok(invite_details) = self.inner.invite_details().await {
if let Some(inviter) = invite_details.inviter {
server_names.push(inviter.user_id().server_name().to_owned());
}
}
if server_names.is_empty()
&& let Ok(invite_details) = self.inner.invite_details().await
&& let Some(inviter) = invite_details.inviter
{
server_names.push(inviter.user_id().server_name().to_owned());
}
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
@@ -1238,39 +1237,18 @@ impl Room {
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
}
/// Retrieve a list of all the threads for the current room.
/// Creates a new [`ThreadListService`] for this room.
///
/// Since this client-server API is paginated, the return type may include a
/// token used to resuming back-pagination into the list of results, in
/// [`ThreadRoots::prev_batch_token`]. This token can be passed to the next
/// call to this function, through the `from` field of
/// [`ListThreadsOptions`].
pub async fn list_threads(&self, opts: ListThreadsOptions) -> Result<ThreadRoots, ClientError> {
let inner_opts = SdkListThreadsOptions {
include_threads: match opts.include_threads {
IncludeThreads::All => SdkIncludeThreads::All,
IncludeThreads::Participated => SdkIncludeThreads::Participated,
},
from: opts.from,
limit: opts.limit.and_then(ruma::UInt::new),
};
/// The returned service provides a reactive, paginated list of thread roots
/// for the room. Use [`ThreadListService::paginate`] to load pages and
/// [`ThreadListService::subscribe_to_items_updates`] /
/// [`ThreadListService::subscribe_to_pagination_state_updates`] to observe
/// changes.
pub fn thread_list_service(&self) -> Arc<ThreadListService> {
// `no reactor running` panics
let _guard = get_runtime_handle().enter();
let roots = self.inner.list_threads(inner_opts).await?;
Ok(ThreadRoots {
chunk: roots
.chunk
.into_iter()
.filter_map(|timeline_event| {
timeline_event
.raw()
.deserialize()
.ok()
.map(|any_timeline_event| TimelineEvent(Box::new(any_timeline_event)))
})
.collect(),
prev_batch_token: roots.prev_batch_token,
})
Arc::new(ThreadListService::new(&self.inner))
}
/// Either loads the event associated with the `event_id` from the event
@@ -1290,74 +1268,6 @@ impl Room {
}
}
/// A thread subscription (MSC4306).
#[derive(uniffi::Record)]
pub struct ThreadSubscription {
/// Whether the thread subscription happened automatically (e.g. after a
/// mention) or if it was manually requested by the user.
automatic: bool,
}
/// Options for [Room::list_threads].
#[derive(Debug, Clone, uniffi::Record)]
pub struct ListThreadsOptions {
/// An extra filter to select which threads should be returned.
pub include_threads: IncludeThreads,
/// The token to start returning events from.
///
/// This token can be obtained from a [`ThreadRoots::prev_batch_token`]
/// returned by a previous call to [`Room::list_threads()`].
///
/// If `from` isn't provided the homeserver shall return a list of thread
/// roots from end of the timeline history.
pub from: Option<String>,
/// The maximum number of events to return.
///
/// Default: 10.
pub limit: Option<u64>,
}
/// Which threads to include in the response.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum IncludeThreads {
/// `all`
///
/// Include all thread roots found in the room.
///
/// This is the default.
All,
/// `participated`
///
/// Only include thread roots for threads where
/// [`current_user_participated`] is `true`.
///
/// [`current_user_participated`]: https://spec.matrix.org/latest/client-server-api/#server-side-aggregation-of-mthread-relationships
Participated,
}
/// The result of a [`Room::list_threads`] query.
///
/// This is a wrapper around the Ruma equivalent, with events decrypted if needs
/// be.
#[derive(uniffi::Object)]
pub struct ThreadRoots {
/// The events that are thread roots in the current batch.
pub chunk: Vec<TimelineEvent>,
/// Token to paginate backwards in a subsequent query to
/// [`Room::list_threads`].
pub prev_batch_token: Option<String>,
}
/// A listener for receiving new live location shares in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
}
/// A listener for receiving call decline events in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait CallDeclineListener: SyncOutsideWasm + SendOutsideWasm {
@@ -1724,23 +1634,30 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
type Error = ClientError;
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
fn draft_thumbnail(
thumbnail_info: Option<ThumbnailInfo>,
thumbnail_source: Option<UploadSource>,
) -> Result<Option<DraftThumbnail>, ClientError> {
if let Some(info) = thumbnail_info
&& let Some(source) = thumbnail_source
{
let (data, filename) = read_upload_source(source)?;
Ok(Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
}))
} else {
Ok(None)
}
}
match value {
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
let (data, filename) = read_upload_source(source)?;
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let (data, filename) = read_upload_source(source)?;
Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
})
}
_ => None,
};
Ok(Self {
filename,
content: DraftAttachmentContent::Image {
@@ -1750,26 +1667,12 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
width: image_info.width,
height: image_info.height,
blurhash: image_info.blurhash,
thumbnail,
thumbnail: draft_thumbnail(image_info.thumbnail_info, thumbnail_source)?,
},
})
}
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
let (data, filename) = read_upload_source(source)?;
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let (data, filename) = read_upload_source(source)?;
Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
})
}
_ => None,
};
Ok(Self {
filename,
content: DraftAttachmentContent::Video {
@@ -1780,7 +1683,7 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
height: video_info.height,
duration: video_info.duration,
blurhash: video_info.blurhash,
thumbnail,
thumbnail: draft_thumbnail(video_info.thumbnail_info, thumbnail_source)?,
},
})
}
@@ -1,9 +1,23 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use anyhow::Result;
use ruma::{
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
OwnedUserId, UserId,
events::{TimelineEventType, room::power_levels::RoomPowerLevels as RumaPowerLevels},
};
use crate::{
+51 -2
View File
@@ -1,6 +1,20 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use matrix_sdk::{EncryptionState, RoomState};
use matrix_sdk::{CallIntentConsensus, EncryptionState, RoomState};
use tracing::warn;
use crate::{
@@ -8,11 +22,35 @@ use crate::{
error::ClientError,
notification_settings::RoomNotificationMode,
room::{
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom, power_levels::RoomPowerLevels,
},
room_member::RoomMember,
ruma::RtcCallIntent,
};
#[derive(Clone, uniffi::Enum)]
pub enum RtcCallIntentConsensus {
Full(RtcCallIntent),
Partial { intent: RtcCallIntent, agreeing_count: u64, total_count: u64 },
None,
}
impl From<CallIntentConsensus> for RtcCallIntentConsensus {
fn from(value: CallIntentConsensus) -> Self {
match value {
CallIntentConsensus::Full(intent) => RtcCallIntentConsensus::Full(intent.into()),
CallIntentConsensus::Partial { intent, agreeing_count, total_count } => {
RtcCallIntentConsensus::Partial {
intent: intent.into(),
agreeing_count,
total_count,
}
}
CallIntentConsensus::None => RtcCallIntentConsensus::None,
}
}
}
#[derive(uniffi::Record)]
pub struct RoomInfo {
id: String,
@@ -35,6 +73,7 @@ pub struct RoomInfo {
/// If present, it means the room has been archived/upgraded.
successor_room: Option<SuccessorRoom>,
is_favourite: bool,
is_low_priority: bool,
canonical_alias: Option<String>,
alternative_aliases: Vec<String>,
membership: Membership,
@@ -48,11 +87,13 @@ pub struct RoomInfo {
active_members_count: u64,
invited_members_count: u64,
joined_members_count: u64,
service_members: Vec<String>,
highlight_count: u64,
notification_count: u64,
cached_user_defined_notification_mode: Option<RoomNotificationMode>,
has_room_call: bool,
active_room_call_participants: Vec<String>,
active_room_call_consensus_intent: RtcCallIntentConsensus,
/// Whether this room has been explicitly marked as unread
is_marked_unread: bool,
/// "Interesting" messages received in that room, independently of the
@@ -119,6 +160,7 @@ impl RoomInfo {
is_space: room.is_space(),
successor_room: room.successor_room().map(Into::into),
is_favourite: room.is_favourite(),
is_low_priority: room.is_low_priority(),
canonical_alias: room.canonical_alias().map(Into::into),
alternative_aliases: room.alt_aliases().into_iter().map(Into::into).collect(),
membership: room.state().into(),
@@ -138,6 +180,12 @@ impl RoomInfo {
active_members_count: room.active_members_count(),
invited_members_count: room.invited_members_count(),
joined_members_count: room.joined_members_count(),
service_members: room
.service_members()
.iter()
.flatten()
.map(|m| m.to_string())
.collect(),
highlight_count: unread_notification_counts.highlight_count,
notification_count: unread_notification_counts.notification_count,
cached_user_defined_notification_mode: room
@@ -149,6 +197,7 @@ impl RoomInfo {
.iter()
.map(|u| u.to_string())
.collect(),
active_room_call_consensus_intent: room.active_room_call_consensus_intent().into(),
is_marked_unread: room.is_marked_unread(),
num_unread_messages: room.num_unread_messages(),
num_unread_notifications: room.num_unread_notifications(),
+14
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use matrix_sdk::RoomDisplayName;
/// Verifies the passed `String` matches the expected room alias format:
@@ -1,4 +1,3 @@
// Copyright 2024 Mauro Romito
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -10,7 +9,7 @@
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// See the License for that specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc};
+27 -25
View File
@@ -1,32 +1,46 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
#![allow(deprecated)]
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use futures_util::{StreamExt, pin_mut};
use matrix_sdk::{
ruma::{
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
RoomId,
},
Room as SdkRoom,
ruma::{
RoomId,
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
room_list_service::filters::{
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_identifiers,
new_filter_invite, new_filter_joined, new_filter_low_priority, new_filter_non_left,
new_filter_none, new_filter_normalized_match_room_name, new_filter_not, new_filter_space,
new_filter_unread, BoxedFilterFn, RoomCategory,
BoxedFilterFn, RoomCategory, new_filter_all, new_filter_any, new_filter_category,
new_filter_deduplicate_versions, new_filter_favourite, new_filter_fuzzy_match_room_name,
new_filter_identifiers, new_filter_invite, new_filter_joined, new_filter_low_priority,
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
new_filter_not, new_filter_space, new_filter_unread,
},
unable_to_decrypt_hook::UtdHookManager,
};
use crate::{
TaskHandle,
room::{Membership, Room},
runtime::get_runtime_handle,
TaskHandle,
};
#[derive(Debug, thiserror::Error, uniffi::Error)]
@@ -168,15 +182,6 @@ impl RoomList {
self: Arc<Self>,
page_size: u32,
listener: Box<dyn RoomListEntriesListener>,
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
self.entries_with_dynamic_adapters_with(page_size, false, listener)
}
fn entries_with_dynamic_adapters_with(
self: Arc<Self>,
page_size: u32,
enable_latest_event_sorter: bool,
listener: Box<dyn RoomListEntriesListener>,
) -> Arc<RoomListEntriesWithDynamicAdaptersResult> {
let this = self;
@@ -214,7 +219,7 @@ impl RoomList {
// Get a reference to `this`. It is only borrowed, it's not moved.
let this =
// SAFETY: `ptr` is correct aligned, the `this` field is correctly aligned,
// SAFETY: `ptr` is correctly aligned, the `this` field is correctly aligned,
// is dereferenceable and points to a correctly initialized value as done
// in the previous line.
unsafe { addr_of_mut!((*ptr).this).as_ref() }
@@ -225,10 +230,7 @@ impl RoomList {
// borrowing `this`, which is going to live long enough since it will live as
// long as `entries_stream` and `dynamic_entries_controller`.
let (entries_stream, dynamic_entries_controller) =
this.inner.entries_with_dynamic_adapters_with(
page_size.try_into().unwrap(),
enable_latest_event_sorter,
);
this.inner.entries_with_dynamic_adapters(page_size.try_into().unwrap());
// FFI dance to make those values consumable by foreign language, nothing fancy
// here, that's the real code for this method.
+1 -1
View File
@@ -1,5 +1,5 @@
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
use ruma::{UserId, events::room::power_levels::UserPowerLevel};
use crate::error::{ClientError, NotYetImplemented};
+15 -1
View File
@@ -1,5 +1,19 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use matrix_sdk::{Client, room_preview::RoomPreview as SdkRoomPreview};
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
use crate::{
+65 -25
View File
@@ -21,8 +21,13 @@ use std::{
use extension_trait::extension_trait;
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
use ruma::{
assign,
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
OwnedRoomId, OwnedUserId, UInt, UserId, assign,
events::{
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
RoomAccountDataEventType as RumaRoomAccountDataEventType,
direct::DirectEventContent,
fully_read::FullyReadEventContent,
identity_server::IdentityServerEventContent,
@@ -36,6 +41,8 @@ use ruma::{
poll::start::PollKind as RumaPollKind,
push_rules::PushRulesEventContent,
room::{
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
message::{
AudioInfo as RumaAudioInfo,
AudioMessageEventContent as RumaAudioMessageEventContent,
@@ -53,10 +60,10 @@ use ruma::{
VideoInfo as RumaVideoInfo,
VideoMessageEventContent as RumaVideoMessageEventContent,
},
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
},
rtc::notification::NotificationType as RumaNotificationType,
rtc::notification::{
CallIntent as RumaCallIntent, NotificationType as RumaNotificationType,
},
secret_storage::{
default_key::SecretStorageDefaultKeyEventContent,
key::{
@@ -70,10 +77,6 @@ use ruma::{
TagEventContent, TagInfo as RumaTagInfo, TagName as RumaTagName,
UserTagName as RumaUserTagName,
},
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
RoomAccountDataEvent as RumaRoomAccountDataEvent,
RoomAccountDataEventType as RumaRoomAccountDataEventType,
},
matrix_uri::MatrixId as RumaMatrixId,
push::{
@@ -81,8 +84,6 @@ use ruma::{
Ruleset as RumaRuleset, SimplePushRule as RumaSimplePushRule,
},
serde::JsonObject,
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
OwnedRoomId, OwnedUserId, UInt, UserId,
};
use tracing::info;
@@ -389,11 +390,7 @@ pub enum MessageType {
/// is its own field.
/// - if a media only has a filename, then body is the filename.
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
if let Some(caption) = caption {
(caption, Some(filename))
} else {
(filename, None)
}
if let Some(caption) = caption { (caption, Some(filename)) } else { (filename, None) }
}
impl TryFrom<MessageType> for RumaMessageType {
@@ -470,11 +467,7 @@ impl TryFrom<RumaMessageType> for MessageType {
geo_uri: c.geo_uri,
description,
zoom_level: zoom_level.and_then(|z| z.get().try_into().ok()),
asset: c.asset.and_then(|a| match a.type_ {
RumaAssetType::Self_ => Some(AssetType::Sender),
RumaAssetType::Pin => Some(AssetType::Pin),
_ => None,
}),
asset: c.asset.map(|a| a.type_).into(),
},
}
}
@@ -510,6 +503,31 @@ impl From<RtcNotificationType> for RumaNotificationType {
}
}
#[derive(Clone, uniffi::Enum)]
pub enum RtcCallIntent {
Video,
Audio,
}
impl From<RumaCallIntent> for RtcCallIntent {
fn from(val: RumaCallIntent) -> Self {
match val {
RumaCallIntent::Audio => Self::Audio,
// No support for custom intents, so we can just use video as default
_ => Self::Video,
}
}
}
impl From<RtcCallIntent> for RumaCallIntent {
fn from(value: RtcCallIntent) -> Self {
match value {
RtcCallIntent::Video => RumaCallIntent::Video,
RtcCallIntent::Audio => RumaCallIntent::Audio,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct EmoteMessageContent {
pub body: String,
@@ -543,7 +561,7 @@ impl TryFrom<RumaImageMessageEventContent> for ImageMessageContent {
fn try_from(value: RumaImageMessageEventContent) -> Result<Self, Self::Error> {
Ok(Self {
filename: value.filename().to_owned(),
caption: value.caption().map(ToString::to_string),
caption: value.caption().map(str::to_owned),
formatted_caption: value.formatted_caption().map(Into::into),
source: Arc::new(value.source.try_into()?),
info: value.info.as_deref().map(TryInto::try_into).transpose()?,
@@ -582,7 +600,7 @@ impl TryFrom<RumaAudioMessageEventContent> for AudioMessageContent {
fn try_from(value: RumaAudioMessageEventContent) -> Result<Self, Self::Error> {
Ok(Self {
filename: value.filename().to_owned(),
caption: value.caption().map(ToString::to_string),
caption: value.caption().map(str::to_owned),
formatted_caption: value.formatted_caption().map(Into::into),
source: Arc::new(value.source.try_into()?),
info: value.info.as_deref().map(Into::into),
@@ -619,7 +637,7 @@ impl TryFrom<RumaVideoMessageEventContent> for VideoMessageContent {
fn try_from(value: RumaVideoMessageEventContent) -> Result<Self, Self::Error> {
Ok(Self {
filename: value.filename().to_owned(),
caption: value.caption().map(ToString::to_string),
caption: value.caption().map(str::to_owned),
formatted_caption: value.formatted_caption().map(Into::into),
source: Arc::new(value.source.try_into()?),
info: value.info.as_deref().map(TryInto::try_into).transpose()?,
@@ -654,7 +672,7 @@ impl TryFrom<RumaFileMessageEventContent> for FileMessageContent {
fn try_from(value: RumaFileMessageEventContent) -> Result<Self, Self::Error> {
Ok(Self {
filename: value.filename().to_owned(),
caption: value.caption().map(ToString::to_string),
caption: value.caption().map(str::to_owned),
formatted_caption: value.formatted_caption().map(Into::into),
source: Arc::new(value.source.try_into()?),
info: value.info.as_deref().map(TryInto::try_into).transpose()?,
@@ -900,13 +918,14 @@ pub struct LocationContent {
pub geo_uri: String,
pub description: Option<String>,
pub zoom_level: Option<u8>,
pub asset: Option<AssetType>,
pub asset: AssetType,
}
#[derive(Clone, uniffi::Enum)]
pub enum AssetType {
Sender,
Pin,
Unknown,
}
impl From<AssetType> for RumaAssetType {
@@ -914,6 +933,26 @@ impl From<AssetType> for RumaAssetType {
match value {
AssetType::Sender => Self::Self_,
AssetType::Pin => Self::Pin,
_ => panic!("Invalid asset type"),
}
}
}
impl From<RumaAssetType> for AssetType {
fn from(value: RumaAssetType) -> Self {
match value {
RumaAssetType::Self_ => Self::Sender,
RumaAssetType::Pin => Self::Pin,
_ => Self::Unknown,
}
}
}
impl From<Option<RumaAssetType>> for AssetType {
fn from(value: Option<RumaAssetType>) -> Self {
match value {
None => Self::Sender,
Some(asset_type) => asset_type.into(),
}
}
}
@@ -1670,6 +1709,7 @@ pub enum RoomAccountDataEvent {
/// The name of a tag.
#[derive(Clone, PartialEq, Eq, Hash, uniffi::Enum)]
#[uniffi::export(Eq, Hash)]
pub enum TagName {
/// `m.favourite`: The user's favorite rooms.
Favorite,
+1 -1
View File
@@ -39,7 +39,7 @@ mod sys {
mod sys {
use std::future::Future;
use matrix_sdk_common::executor::{spawn, JoinHandle};
use matrix_sdk_common::executor::{JoinHandle, spawn};
/// A dummy guard that does nothing when dropped.
/// This is used for the Wasm implementation to match
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use matrix_sdk::{
deserialized_responses::TimelineEvent,
message_search::{
GlobalSearchIterator as SdkGlobalSearchIterator,
RoomSearchIterator as SdkRoomSearchIterator, SearchError as SdkSearchError,
},
};
use matrix_sdk_ui::timeline::TimelineDetails;
use tokio::sync::Mutex;
use crate::{
client::Client,
error::ClientError,
room::Room,
timeline::{ProfileDetails, TimelineItemContent},
utils::Timestamp,
};
#[derive(uniffi::Error, thiserror::Error, Debug)]
pub enum SearchError {
#[error("Failed to search through the index: {0}")]
IndexError(String),
#[error("Failed to load event content for search result: {0}")]
EventLoadError(String),
}
impl From<SdkSearchError> for SearchError {
fn from(err: SdkSearchError) -> Self {
match err {
SdkSearchError::IndexError(err) => SearchError::IndexError(err.to_string()),
SdkSearchError::EventLoadError(err) => SearchError::EventLoadError(err.to_string()),
}
}
}
#[matrix_sdk_ffi_macros::export]
impl Room {
/// Search for messages in this room matching the given query, returning an
/// iterator over the results that yields `num_results_per_batch` results at
/// a time.
pub fn search_messages(&self, query: String, num_results_per_batch: u32) -> RoomSearchIterator {
RoomSearchIterator {
sdk_room: self.inner.clone(),
inner: Mutex::new(self.inner.search_messages(query, num_results_per_batch as usize)),
}
}
}
#[derive(uniffi::Object)]
pub struct RoomSearchIterator {
sdk_room: matrix_sdk::room::Room,
inner: Mutex<SdkRoomSearchIterator>,
}
#[matrix_sdk_ffi_macros::export]
impl RoomSearchIterator {
/// Return a list of events for the next batch of search results, or `None`
/// if there are no more results.
pub async fn next_events(&self) -> Result<Option<Vec<RoomSearchResult>>, SearchError> {
let Some(events) = self.inner.lock().await.next_events().await? else {
return Ok(None);
};
let mut results = Vec::with_capacity(events.len());
for event in events {
if let Some(result) = RoomSearchResult::from(&self.sdk_room, event).await {
results.push(result);
}
}
results.shrink_to_fit();
Ok(Some(results))
}
}
#[derive(Clone, uniffi::Record)]
pub struct RoomSearchResult {
event_id: String,
sender: String,
sender_profile: ProfileDetails,
content: TimelineItemContent,
timestamp: Timestamp,
}
impl RoomSearchResult {
async fn from(room: &matrix_sdk::room::Room, event: TimelineEvent) -> Option<Self> {
let sender = event.sender()?;
let event_id = event.event_id().unwrap().to_string();
let timestamp =
event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into();
let content = matrix_sdk_ui::timeline::TimelineItemContent::from_event(room, event).await?;
let profile = TimelineDetails::from_initial_value(
matrix_sdk_ui::timeline::Profile::load(room, &sender).await,
);
Some(Self {
event_id,
sender: sender.to_string(),
sender_profile: ProfileDetails::from(profile),
content: TimelineItemContent::from(content),
timestamp,
})
}
}
#[derive(Clone, uniffi::Enum)]
pub enum SearchRoomFilter {
/// All the joined rooms (= DMs + non-DMs).
Rooms,
/// Only joined DM rooms.
Dms,
/// Only joined non-DM (group) rooms.
NonDms,
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Search across all all rooms for the given query, returning an iterator
/// over the results.
pub async fn search_messages(
&self,
query: String,
filter: SearchRoomFilter,
num_results_per_batch: u32,
) -> Result<GlobalSearchIterator, ClientError> {
let sdk_client = (*self.inner).clone();
let mut search = sdk_client.search_messages(query, num_results_per_batch as usize);
match filter {
SearchRoomFilter::Rooms => {}
SearchRoomFilter::Dms => search = search.only_dm_rooms().await?,
SearchRoomFilter::NonDms => search = search.no_dms().await?,
}
Ok(GlobalSearchIterator { sdk_client, inner: Mutex::new(search.build()) })
}
}
#[derive(uniffi::Record)]
pub struct GlobalSearchResult {
room_id: String,
result: RoomSearchResult,
}
#[derive(uniffi::Object)]
pub struct GlobalSearchIterator {
sdk_client: matrix_sdk::Client,
inner: Mutex<SdkGlobalSearchIterator>,
}
#[matrix_sdk_ffi_macros::export]
impl GlobalSearchIterator {
/// Return a list of events for the next batch of search results, or `None`
/// if there are no more results.
pub async fn next_events(&self) -> Result<Option<Vec<GlobalSearchResult>>, SearchError> {
let Some(events) = self.inner.lock().await.next_events().await? else {
return Ok(None);
};
let mut results = Vec::with_capacity(events.len());
for (room_id, event) in events {
let Some(room) = self.sdk_client.get_room(&room_id) else {
continue;
};
if let Some(result) = RoomSearchResult::from(&room, event).await {
results.push(GlobalSearchResult { room_id: room_id.to_string(), result });
}
}
results.shrink_to_fit();
Ok(Some(results))
}
}
@@ -1,14 +1,28 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::{Arc, RwLock};
use futures_util::StreamExt;
use matrix_sdk::{
Account,
encryption::{
Encryption,
identities::UserIdentity,
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
Encryption,
},
ruma::events::key::verification::VerificationMethod,
Account,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::UserId;
@@ -232,16 +246,15 @@ impl SessionVerificationController {
sender: &UserId,
flow_id: impl AsRef<str>,
) {
if sender != self.user_identity.user_id() {
if let Some(status) = self.encryption.cross_signing_status().await {
if !status.is_complete() {
warn!(
"Cannot verify other users until our own device's cross-signing status \
is complete: {status:?}"
);
return;
}
}
if sender != self.user_identity.user_id()
&& let Some(status) = self.encryption.cross_signing_status().await
&& !status.is_complete()
{
warn!(
"Cannot verify other users until our own device's cross-signing status \
is complete: {status:?}"
);
return;
}
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
@@ -264,7 +277,7 @@ impl SessionVerificationController {
sender_profile,
flow_id: request.flow_id().into(),
device_id: other_device_data.device_id().into(),
device_display_name: other_device_data.display_name().map(str::to_string),
device_display_name: other_device_data.display_name().map(str::to_owned),
first_seen_timestamp: other_device_data.first_time_seen_ts().into(),
});
}
@@ -276,15 +289,10 @@ impl SessionVerificationController {
) -> Result<(), ClientError> {
if let Some(ongoing_verification_request) =
self.verification_request.read().unwrap().clone()
&& !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
if !ongoing_verification_request.is_done()
&& !ongoing_verification_request.is_cancelled()
{
return Err(ClientError::from_str(
"There is another verification flow ongoing.",
None,
));
}
return Err(ClientError::from_str("There is another verification flow ongoing.", None));
}
*self.verification_request.write().unwrap() = Some(verification_request.clone());
+4 -4
View File
@@ -15,23 +15,23 @@
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use futures_util::{StreamExt, pin_mut};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::spaces::{
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
room_list::SpaceRoomListPaginationState,
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
SpaceService as UISpaceService,
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
room_list::SpaceRoomListPaginationState,
};
use ruma::RoomId;
use crate::{
TaskHandle,
client::JoinRule,
error::ClientError,
room::{Membership, RoomHero},
room_preview::RoomType,
runtime::get_runtime_handle,
TaskHandle,
};
/// The main entry point into the Spaces facilities.
+14
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
#[cfg(feature = "sqlite")]
use std::path::PathBuf;
+24 -8
View File
@@ -26,8 +26,8 @@ use matrix_sdk_ui::{
};
use crate::{
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
runtime::get_runtime_handle, TaskHandle,
TaskHandle, error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
runtime::get_runtime_handle,
};
#[derive(uniffi::Enum)]
@@ -115,12 +115,6 @@ impl SyncServiceBuilder {
#[matrix_sdk_ffi_macros::export]
impl SyncServiceBuilder {
pub fn with_cross_process_lock(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_cross_process_lock();
Arc::new(Self { builder, ..this })
}
/// Enable the "offline" mode for the [`SyncService`].
pub fn with_offline_mode(self: Arc<Self>) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
@@ -134,6 +128,28 @@ impl SyncServiceBuilder {
Arc::new(Self { builder, ..this })
}
/// Set a custom Sliding Sync connection ID for the room list service.
///
/// By default [`matrix_sdk_ui::room_list_service::DEFAULT_CONNECTION_ID`]
/// is used. Set a different value for secondary processes such as iOS
/// Share Extensions that are not meant to reuse the main app's
/// connection.
pub fn with_room_list_connection_id(self: Arc<Self>, connection_id: String) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_room_list_conn_id(connection_id);
Arc::new(Self { builder, ..this })
}
/// Set a custom timeline limit for the room list service.
///
/// When set, overrides the default timeline limit of
/// [`matrix_sdk_ui::room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`].
pub fn with_room_list_timeline_limit(self: Arc<Self>, limit: u32) -> Arc<Self> {
let this = unwrap_or_clone_arc(self);
let builder = this.builder.with_room_list_timeline_limit(limit);
Arc::new(Self { builder, ..this })
}
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
let this = unwrap_or_clone_arc(self);
Ok(Arc::new(SyncService {
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::time::Duration;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
/// A listener for the sync loop.
///
/// Called after each successful sync response when using
/// [`Client::sync_v2`](crate::client::Client::sync_v2).
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncListenerV2: SyncOutsideWasm + SendOutsideWasm {
/// Called after each successful sync response.
fn on_update(&self, response: SyncResponseV2);
}
/// Settings for a sync v2 call.
#[derive(uniffi::Record)]
pub struct SyncSettingsV2 {
/// Timeout in milliseconds for the server long-poll.
/// If not set, defaults to 30 seconds.
#[uniffi(default = None)]
pub timeout_ms: Option<u64>,
/// Whether to request full state on the first sync.
#[uniffi(default = false)]
pub full_state: bool,
}
impl From<SyncSettingsV2> for matrix_sdk::config::SyncSettings {
fn from(value: SyncSettingsV2) -> Self {
let mut settings = matrix_sdk::config::SyncSettings::new();
if let Some(timeout_ms) = value.timeout_ms {
settings = settings.timeout(Duration::from_millis(timeout_ms));
}
if value.full_state {
settings = settings.full_state(true);
}
settings
}
}
/// The response from a sync v2 call.
#[derive(uniffi::Record)]
pub struct SyncResponseV2 {
/// The batch token to supply in the `since` param of the next `/sync`
/// request.
pub next_batch: String,
/// Updates to rooms.
pub rooms: SyncResponseRoomsV2,
}
/// Room updates from a sync v2 response.
#[derive(uniffi::Record)]
pub struct SyncResponseRoomsV2 {
/// Room IDs of rooms the user has been invited to.
pub invited: Vec<String>,
/// Room IDs of joined rooms that had updates.
pub joined: Vec<String>,
/// Room IDs of rooms the user has left.
pub left: Vec<String>,
/// Room IDs of rooms the user has knocked on.
pub knocked: Vec<String>,
}
impl From<matrix_sdk::sync::SyncResponse> for SyncResponseV2 {
fn from(value: matrix_sdk::sync::SyncResponse) -> Self {
Self {
next_batch: value.next_batch,
rooms: SyncResponseRoomsV2 {
invited: value.rooms.invited.keys().map(ToString::to_string).collect(),
joined: value.rooms.joined.keys().map(ToString::to_string).collect(),
left: value.rooms.left.keys().map(ToString::to_string).collect(),
knocked: value.rooms.knocked.keys().map(ToString::to_string).collect(),
},
}
}
}
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::executor::JoinHandle;
use tracing::debug;
@@ -1,12 +1,26 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use matrix_sdk_ui::timeline::{
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
};
use ruma::{
events::{AnySyncTimelineEvent, TimelineEventType},
EventId,
events::{AnySyncTimelineEvent, TimelineEventType},
};
use super::FocusEventError;
+89 -49
View File
@@ -17,12 +17,12 @@ use std::collections::HashMap;
use matrix_sdk::room::power_levels::power_level_user_changes;
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
use ruma::events::{
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, FullStateEventContent,
StateEventContentChange, room::history_visibility::HistoryVisibility as RumaHistoryVisibility,
};
use crate::{
client::JoinRule, event::TimelineEventType, timeline::msg_like::MsgLikeContent,
utils::Timestamp,
client::JoinRule, event::TimelineEventType, ruma::AssetType,
timeline::msg_like::MsgLikeContent, utils::Timestamp,
};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
@@ -40,11 +40,13 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
Content::CallInvite => TimelineItemContent::CallInvite,
Content::RtcNotification => TimelineItemContent::RtcNotification,
Content::RtcNotification { call_intent } => TimelineItemContent::RtcNotification {
call_intent: call_intent.map(|s| s.to_string()),
},
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
StateEventContentChange::Original { content, .. } => content.reason.clone(),
_ => None,
};
TimelineItemContent::RoomMembership {
@@ -133,8 +135,8 @@ pub enum HistoryVisibility {
},
}
impl From<RumaHistoryVisibility> for HistoryVisibility {
fn from(value: RumaHistoryVisibility) -> Self {
impl From<&RumaHistoryVisibility> for HistoryVisibility {
fn from(value: &RumaHistoryVisibility) -> Self {
match value {
RumaHistoryVisibility::Invited => Self::Invited,
RumaHistoryVisibility::Joined => Self::Joined,
@@ -159,7 +161,9 @@ pub enum TimelineItemContent {
content: MsgLikeContent,
},
CallInvite,
RtcNotification,
RtcNotification {
call_intent: Option<String>,
},
RoomMembership {
user_id: String,
user_display_name: Option<String>,
@@ -265,18 +269,17 @@ pub enum OtherState {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar {
url: Option<String>,
},
RoomCanonicalAlias,
RoomCreate {
federate: Option<bool>,
federate: bool,
},
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility {
history_visibility: Option<HistoryVisibility>,
history_visibility: HistoryVisibility,
},
RoomJoinRules {
join_rule: Option<JoinRule>,
@@ -292,7 +295,7 @@ pub enum OtherState {
previous_events: Option<HashMap<TimelineEventType, i64>>,
users: HashMap<String, i64>,
previous_users: Option<HashMap<String, i64>>,
thresholds: Option<PowerLevelChanges>,
thresholds: PowerLevelChanges,
previous_thresholds: Option<PowerLevelChanges>,
},
RoomServerAcl,
@@ -310,16 +313,59 @@ pub enum OtherState {
},
}
impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
/// FFI representation of a single location update from a beacon event.
#[derive(Clone, uniffi::Record)]
pub struct BeaconInfo {
/// The geo URI carrying the user's coordinates
/// (e.g. `"geo:51.5008,0.1247;u=35"`).
pub geo_uri: String,
/// Timestamp (ms since Unix Epoch) of this location update.
pub ts: Timestamp,
/// An optional human-readable description of the location.
pub description: Option<String>,
}
/// FFI representation of a live location sharing session (MSC3489).
///
/// Corresponds to a `org.matrix.msc3672.beacon_info` state event in the
/// timeline. Location updates are aggregated here as they arrive.
#[derive(Clone, uniffi::Record)]
pub struct LiveLocationContent {
/// Whether this sharing session is currently active.
pub is_live: bool,
/// The timestamp when this live location sharing session started
/// (from the `org.matrix.msc3488.ts` field of the originating
/// `beacon_info` state event).
///
/// This marks the *beginning* of the session. The session expires at
/// `ts + timeout_ms`.
pub ts: Timestamp,
/// An optional human-readable label for this sharing session.
pub description: Option<String>,
/// Duration of the session in milliseconds.
pub timeout_ms: u64,
/// The asset type of the beacon (e.g. `Sender` for the user's own
/// location, `Pin` for a fixed point of interest).
pub asset_type: AssetType,
/// All location updates received so far, sorted oldest-first.
pub locations: Vec<BeaconInfo>,
}
impl From<&matrix_sdk_ui::timeline::AnyOtherStateEventContentChange> for OtherState {
fn from(content: &matrix_sdk_ui::timeline::AnyOtherStateEventContentChange) -> Self {
use matrix_sdk::ruma::events::StateEventContentChange as FullContent;
use matrix_sdk_ui::timeline::AnyOtherStateEventContentChange as Content;
match content {
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
Content::RoomAliases(_) => Self::RoomAliases,
Content::RoomAvatar(c) => {
let url = match c {
FullContent::Original { content, .. } => {
@@ -332,8 +378,8 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
Content::RoomCreate(c) => {
let federate = match c {
FullContent::Original { content, .. } => Some(content.federate),
FullContent::Redacted(_) => None,
FullContent::Original { content, .. } => content.federate,
FullContent::Redacted(content) => content.federate,
};
Self::RoomCreate { federate }
}
@@ -341,25 +387,22 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
Content::RoomHistoryVisibility(c) => {
let history_visibility = match c {
FullContent::Original { content, .. } => {
Some(content.history_visibility.clone().into())
}
FullContent::Redacted(_) => None,
FullContent::Original { content, .. } => &content.history_visibility,
FullContent::Redacted(content) => &content.history_visibility,
};
Self::RoomHistoryVisibility { history_visibility }
Self::RoomHistoryVisibility { history_visibility: history_visibility.into() }
}
Content::RoomJoinRules(c) => {
let join_rule = match c {
FullContent::Original { content, .. } => {
match content.join_rule.clone().try_into() {
Ok(jr) => Some(jr),
Err(err) => {
tracing::error!("Failed to convert join rule: {}", err);
None
}
}
let ruma_join_rule = match c {
FullContent::Original { content, .. } => &content.join_rule,
FullContent::Redacted(content) => &content.join_rule,
};
let join_rule = match ruma_join_rule.clone().try_into() {
Ok(jr) => Some(jr),
Err(err) => {
tracing::error!("Failed to convert join rule: {}", err);
None
}
FullContent::Redacted(_) => None,
};
Self::RoomJoinRules { join_rule }
}
@@ -371,8 +414,13 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
Self::RoomName { name }
}
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
Content::RoomPowerLevels(c) => match c {
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
Content::RoomPowerLevels(c) => {
let (content, prev_content) = match c.clone() {
FullContent::Original { content, prev_content } => (content, prev_content),
FullContent::Redacted(content) => (content.into(), None),
};
Self::RoomPowerLevels {
events: content
.events
.iter()
@@ -385,7 +433,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
.map(|(k, &v)| (k.clone().into(), v.into()))
.collect()
}),
thresholds: Some(PowerLevelChanges {
thresholds: PowerLevelChanges {
ban: content.ban.into(),
kick: content.kick.into(),
events_default: content.events_default.into(),
@@ -394,7 +442,7 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
state_default: content.state_default.into(),
users_default: content.users_default.into(),
notifications: content.notifications.room.into(),
}),
},
previous_thresholds: prev_content.as_ref().map(|prev_content| {
PowerLevelChanges {
ban: prev_content.ban.into(),
@@ -407,23 +455,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
notifications: prev_content.notifications.room.into(),
}
}),
users: power_level_user_changes(content, prev_content)
users: power_level_user_changes(&content, &prev_content)
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
previous_users: prev_content.as_ref().map(|prev_content| {
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
}),
},
FullContent::Redacted(_) => Self::RoomPowerLevels {
events: Default::default(),
previous_events: None,
users: Default::default(),
previous_users: None,
thresholds: None,
previous_thresholds: None,
},
},
}
}
Content::RoomServerAcl(_) => Self::RoomServerAcl,
Content::RoomThirdPartyInvite(c) => {
let display_name = match c {
+30 -11
View File
@@ -21,7 +21,7 @@ use matrix_sdk::{
attachment::{
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
event_cache::RoomPaginationStatus,
event_cache::PaginationStatus,
room::edit::EditedContent as SdkEditedContent,
};
use matrix_sdk_common::{
@@ -38,8 +38,9 @@ use matrix_sdk_ui::timeline::{
use mime::Mime;
use reply::{EmbeddedEventDetails, InReplyToDetails};
use ruma::{
assign,
EventId, UInt, assign,
events::{
AnyMessageLikeEventContent,
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
poll::{
unstable_end::UnstablePollEndEventContent,
@@ -53,16 +54,13 @@ use ruma::{
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
TextMessageEventContent,
},
AnyMessageLikeEventContent,
},
EventId, UInt,
};
use tokio::sync::Mutex;
use tracing::{error, warn};
use uuid::Uuid;
use self::content::TimelineItemContent;
pub use self::msg_like::MessageContent;
pub use self::{content::TimelineItemContent, msg_like::MessageContent};
use crate::{
error::{ClientError, RoomError},
event::EventOrTransactionId,
@@ -79,6 +77,7 @@ pub mod configuration;
mod content;
mod msg_like;
mod reply;
pub mod threads;
use matrix_sdk::utils::formatted_body_from;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
@@ -284,8 +283,17 @@ impl Timeline {
// be that the listener be called before the initial items have been
// handled by the caller. See #3535 for details.
// First, pass all the items as a reset update.
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
// Note we pass initial items as a reset update, as a way to give the callers a
// unified way to handle the initial batch of items as well as other
// batches, instead of having a separate callback for the initial items.
//
// Start with passing all the items of a *non-empty* timeline as a reset update
// (if the initial items are empty, then the timeline would transition
// from empty to empty, which is a no-op).
if !timeline_items.is_empty() {
listener
.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
}
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(timeline_stream);
@@ -821,7 +829,7 @@ pub trait TimelineListener: SyncOutsideWasm + SendOutsideWasm {
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait PaginationStatusListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, status: RoomPaginationStatus);
fn on_update(&self, status: PaginationStatus);
}
#[derive(Clone, uniffi::Enum)]
@@ -1012,6 +1020,9 @@ pub struct EventTimelineItem {
is_own: bool,
is_editable: bool,
content: TimelineItemContent,
/// The raw Matrix event type string (e.g. `"m.room.message"`), or `None`
/// when the original type is not available (e.g. redacted events).
event_type_raw: Option<String>,
timestamp: Timestamp,
local_send_state: Option<EventSendState>,
local_created_at: Option<u64>,
@@ -1037,6 +1048,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
is_own: item.is_own(),
is_editable: item.is_editable(),
content: item.content().clone().into(),
event_type_raw: item.content().event_type_str(),
timestamp: item.timestamp().into(),
local_send_state: item.send_state().map(|s| s.into()),
local_created_at: item.local_created_at().map(|t| t.0.into()),
@@ -1314,6 +1326,13 @@ impl LazyTimelineItemProvider {
fn contains_only_emojis(&self) -> bool {
self.0.contains_only_emojis()
}
/// Returns the full raw JSON string of the latest version of the event
/// (including edits). Returns `None` for local echoes that haven't been
/// echoed back by the server yet.
fn latest_json(&self) -> Option<String> {
Some(self.0.latest_json()?.json().get().to_owned())
}
}
/// Mimic the [`UiLatestEventValue`] type.
@@ -1387,7 +1406,7 @@ mod galleries {
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
use matrix_sdk_ui::timeline::GalleryConfig;
use mime::Mime;
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
use ruma::{EventId, assign, events::room::message::TextMessageEventContent};
use tokio::sync::Mutex;
use tracing::error;
@@ -1395,7 +1414,7 @@ mod galleries {
error::RoomError,
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
runtime::get_runtime_handle,
timeline::{build_thumbnail_info, Timeline, UploadSource},
timeline::{Timeline, UploadSource, build_thumbnail_info},
};
#[derive(uniffi::Record)]
@@ -15,10 +15,10 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk_base::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
use ruma::events::{MessageLikeEventContent, room::MediaSource as RumaMediaSource};
use super::{
content::Reaction,
content::{BeaconInfo, LiveLocationContent, Reaction},
reply::{EmbeddedEventDetails, InReplyToDetails},
};
use crate::{
@@ -54,6 +54,12 @@ pub enum MsgLikeKind {
/// A custom message like event.
Other { event_type: MessageLikeEventType },
/// A live location sharing session (MSC3489).
///
/// Represents a `org.matrix.msc3672.beacon_info` state event with all
/// aggregated location updates from `org.matrix.msc3672.beacon` events.
LiveLocation { content: LiveLocationContent },
}
/// A special kind of [`super::TimelineItemContent`] that groups together
@@ -195,6 +201,34 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
thread_root,
thread_summary,
},
Kind::LiveLocation(state) => {
let locations = state
.locations()
.iter()
.map(|location| BeaconInfo {
geo_uri: location.geo_uri().to_owned(),
ts: location.ts().into(),
description: location.description().map(ToOwned::to_owned),
})
.collect();
Self {
kind: MsgLikeKind::LiveLocation {
content: LiveLocationContent {
is_live: state.is_live(),
ts: state.ts().into(),
description: state.description().map(ToOwned::to_owned),
timeout_ms: state.timeout().as_millis() as u64,
asset_type: state.asset_type().into(),
locations,
},
},
reactions,
in_reply_to,
thread_root,
thread_summary,
}
}
})
}
}
@@ -14,7 +14,7 @@
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
use super::{content::TimelineItemContent, ProfileDetails};
use super::{ProfileDetails, content::TimelineItemContent};
use crate::{event::EventOrTransactionId, utils::Timestamp};
#[derive(Clone, uniffi::Object)]
@@ -0,0 +1,345 @@
// Copyright 2026 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt::Debug, sync::Arc};
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::room::{
ListThreadsOptions as SdkListThreadsOptions, ThreadSubscription as SdkThreadSubscription,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::timeline::{
RoomExt,
thread_list_service::{
ThreadListItem as UIThreadListItem, ThreadListItemEvent as UIThreadListItemEvent,
ThreadListPaginationState as UIThreadListPaginationState,
ThreadListService as UIThreadListService,
ThreadListServiceError as UIThreadListServiceError,
},
};
use ruma::api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads;
use crate::{
TaskHandle,
error::ClientError,
runtime::get_runtime_handle,
timeline::{ProfileDetails, TimelineItemContent},
utils::Timestamp,
};
/// A thread subscription (MSC4306).
#[derive(uniffi::Record)]
pub struct ThreadSubscription {
/// Whether the thread subscription happened automatically (e.g. after a
/// mention) or if it was manually requested by the user.
pub automatic: bool,
}
impl From<ThreadSubscription> for SdkThreadSubscription {
fn from(subscription: ThreadSubscription) -> Self {
Self { automatic: subscription.automatic }
}
}
/// Options for [`Room::load_thread_list`].
#[derive(Debug, Clone, uniffi::Record)]
pub struct ListThreadsOptions {
/// An extra filter to select which threads should be returned.
pub include_threads: IncludeThreads,
/// The token to start returning events from.
///
/// This token can be obtained from a [`ThreadList::prev_batch_token`]
/// returned by a previous call to [`Room::load_thread_list()`].
///
/// If `from` isn't provided the homeserver shall return a list of thread
/// roots from end of the timeline history.
pub from: Option<String>,
/// The maximum number of events to return.
///
/// Default: 10.
pub limit: Option<u64>,
}
impl From<ListThreadsOptions> for SdkListThreadsOptions {
fn from(opts: ListThreadsOptions) -> Self {
Self {
include_threads: opts.include_threads.into(),
from: opts.from,
limit: opts.limit.and_then(ruma::UInt::new),
}
}
}
/// Which threads to include in the response.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum IncludeThreads {
/// `all`
///
/// Include all thread roots found in the room.
///
/// This is the default.
All,
/// `participated`
///
/// Only include thread roots for threads where
/// [`current_user_participated`] is `true`.
///
/// [`current_user_participated`]: https://spec.matrix.org/latest/client-server-api/#server-side-aggregation-of-mthread-relationships
Participated,
}
impl From<IncludeThreads> for SdkIncludeThreads {
fn from(include_threads: IncludeThreads) -> Self {
match include_threads {
IncludeThreads::All => Self::All,
IncludeThreads::Participated => Self::Participated,
}
}
}
/// Each `ThreadListItem` represents one thread root event in the room. The
/// fields are pre-resolved from the raw homeserver response: the sender's
/// profile is fetched eagerly and the event content is parsed into a
/// `TimelineItemContent` so that consumers can render the item without any
/// additional work.
///
/// `ThreadListItem`s are produced page by page via `Room::load_thread_list()`
/// and are accumulated inside the `ThreadListService` as pages are fetched
/// through `ThreadListService::paginate()`.
#[derive(uniffi::Record)]
pub struct ThreadListItem {
/// The thread root event.
///
/// Contains the event ID, timestamp, sender, sender profile, and parsed
/// content of the thread's root message. Use `root_event.event_id` to open
/// a per-thread `Timeline` or to navigate the user to the thread view.
root_event: ThreadListItemEvent,
/// The latest event in the thread (i.e. the most recent reply), if
/// available.
///
/// Initially populated from the server's bundled thread summary and
/// updated in real time as new events arrive via sync or back-pagination.
latest_event: Option<ThreadListItemEvent>,
/// The number of replies in this thread (excluding the root event).
///
/// Initially populated from the server's bundled thread summary and
/// updated in real time as new events arrive via sync.
num_replies: u32,
}
impl From<UIThreadListItem> for ThreadListItem {
fn from(item: UIThreadListItem) -> Self {
Self {
root_event: item.root_event.into(),
latest_event: item.latest_event.map(Into::into),
num_replies: item.num_replies,
}
}
}
/// Information about an event in a thread (either the root or the latest
/// reply).
#[derive(uniffi::Record)]
pub struct ThreadListItemEvent {
/// The event ID.
pub event_id: String,
/// The timestamp of the event.
pub timestamp: Timestamp,
/// The sender of the event.
pub sender: String,
/// The sender's profile details.
pub sender_profile: ProfileDetails,
/// Whether the event was sent by the current user.
pub is_own: bool,
/// The content of the event, if available.
pub content: Option<TimelineItemContent>,
}
impl From<UIThreadListItemEvent> for ThreadListItemEvent {
fn from(event: UIThreadListItemEvent) -> Self {
Self {
event_id: event.event_id.to_string(),
timestamp: event.timestamp.into(),
sender: event.sender.to_string(),
is_own: event.is_own,
sender_profile: event.sender_profile.into(),
content: event.content.map(Into::into),
}
}
}
/// Listener for changes to the [`ThreadListService`] pagination state.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ThreadListPaginationStateListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, state: UIThreadListPaginationState);
}
/// Listener for changes to the [`ThreadListService`] item list.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ThreadListEntriesListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, diff: Vec<ThreadListUpdate>);
}
/// A diff applied to the observable thread list.
///
/// Mirrors [`eyeball_im::VectorDiff`] for [`ThreadListItem`].
#[derive(uniffi::Enum)]
pub enum ThreadListUpdate {
/// New items were appended at the back.
Append { values: Vec<ThreadListItem> },
/// The list was cleared.
Clear,
/// A new item was prepended at the front.
PushFront { value: ThreadListItem },
/// A new item was appended at the back.
PushBack { value: ThreadListItem },
/// The first item was removed.
PopFront,
/// The last item was removed.
PopBack,
/// An item was inserted at the given position.
Insert { index: u32, value: ThreadListItem },
/// The item at the given position was replaced.
Set { index: u32, value: ThreadListItem },
/// The item at the given position was removed.
Remove { index: u32 },
/// The list was truncated to the given length.
Truncate { length: u32 },
/// The whole list was replaced with new items.
Reset { values: Vec<ThreadListItem> },
}
impl From<VectorDiff<UIThreadListItem>> for ThreadListUpdate {
fn from(diff: VectorDiff<UIThreadListItem>) -> Self {
match diff {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(Into::into).collect() }
}
VectorDiff::Clear => Self::Clear,
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
VectorDiff::PopFront => Self::PopFront,
VectorDiff::PopBack => Self::PopBack,
VectorDiff::Insert { index, value } => {
Self::Insert { index: index as u32, value: value.into() }
}
VectorDiff::Set { index, value } => {
Self::Set { index: index as u32, value: value.into() }
}
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
VectorDiff::Reset { values } => {
Self::Reset { values: values.into_iter().map(Into::into).collect() }
}
}
}
}
/// A high-level, reactive, paginated list of threads for a room.
///
/// `ThreadListService` is the FFI-facing wrapper around
/// [`matrix_sdk_ui::timeline::thread_list_service::ThreadListService`]. It
/// maintains an observable list of [`ThreadListItem`]s and exposes a
/// pagination state publisher, making it straightforward to build reactive UIs
/// on top of the thread list.
///
/// Obtain an instance via [`Room::thread_list_service`].
#[derive(uniffi::Object)]
pub struct ThreadListService {
inner: UIThreadListService,
}
impl ThreadListService {
pub(crate) fn new(room: &matrix_sdk::Room) -> Self {
Self { inner: room.thread_list_service() }
}
}
#[matrix_sdk_ffi_macros::export]
impl ThreadListService {
/// Returns a snapshot of the current pagination state.
pub fn pagination_state(&self) -> UIThreadListPaginationState {
self.inner.pagination_state()
}
/// Subscribes to changes in the pagination state.
///
/// The `listener` is called once for every state transition. The returned
/// [`TaskHandle`] keeps the subscription alive
pub fn subscribe_to_pagination_state_updates(
&self,
listener: Box<dyn ThreadListPaginationStateListener>,
) -> Arc<TaskHandle> {
let mut subscriber = self.inner.subscribe_to_pagination_state_updates();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = subscriber.next().await {
listener.on_update(state);
}
})))
}
/// Returns a snapshot of the current thread list items.
pub fn items(&self) -> Vec<ThreadListItem> {
self.inner.items().into_iter().map(Into::into).collect()
}
/// Subscribes to changes in the thread list.
///
/// The `listener` receives an initial `Reset` diff containing all currently
/// loaded items, followed by subsequent diffs as the list changes.
pub fn subscribe_to_items_updates(
&self,
listener: Box<dyn ThreadListEntriesListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_items_updates();
// Emit the current snapshot immediately so the caller starts with a
// consistent view of the list.
listener.on_update(vec![ThreadListUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
}]);
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(diffs) = stream.next().await {
listener.on_update(diffs.into_iter().map(Into::into).collect());
}
})))
}
/// Fetches the next page of threads and appends the results to the list.
///
/// This is a no-op when the list is already loading or the end has been
/// reached.
pub async fn paginate(&self) -> Result<(), ClientError> {
self.inner.paginate().await.map_err(|e| match e {
UIThreadListServiceError::Sdk(sdk_err) => ClientError::from(sdk_err),
})
}
/// Resets the service back to its initial, empty state.
///
/// Clears all loaded items, discards the pagination token, and sets the
/// state to `Idle { end_reached: false }`. The next call to
/// [`Self::paginate`] will restart from the beginning of the thread list.
pub async fn reset(&self) {
self.inner.reset().await;
}
}
+4 -4
View File
@@ -41,10 +41,10 @@ impl UnableToDecryptHook for UtdHook {
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
// not considered UTDs.
if let Some(duration) = &info.time_to_decrypt {
if *duration < IGNORE_UTD_PERIOD {
return;
}
if let Some(duration) = &info.time_to_decrypt
&& *duration < IGNORE_UTD_PERIOD
{
return;
}
// Report the UTD to the client.
+22
View File
@@ -1,3 +1,17 @@
// Copyright 2025 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for that specific language governing permissions and
// limitations under the License.
use std::sync::{Arc, Mutex};
use language_tags::LanguageTag;
@@ -252,6 +266,7 @@ pub fn get_element_call_required_permissions(
requires_client: true,
update_delayed_event: true,
send_delayed_event: true,
download_files: true,
}
}
@@ -317,6 +332,8 @@ pub struct WidgetCapabilities {
pub update_delayed_event: bool,
/// This allows the widget to send events with a delay.
pub send_delayed_event: bool,
/// This allows the widget to download files (avatars)
pub download_files: bool,
}
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
@@ -327,6 +344,7 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
download_file: value.download_files,
}
}
}
@@ -339,6 +357,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
requires_client: value.requires_client,
update_delayed_event: value.update_delayed_event,
send_delayed_event: value.send_delayed_event,
download_files: value.download_file,
}
}
}
@@ -539,5 +558,8 @@ mod tests {
// RTC decline
cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
// Download avatars
cap_assert("org.matrix.msc4039.download_file");
}
}
+85 -45
View File
@@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.
### Bug Fixes
- Room keys are now rotated whenever the client fully reloads the member list by making a
request to `/members`, which prevents clients using keys that may have been shared under
[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) even if a gappy
sync occurs.
([#6339](https://github.com/matrix-org/matrix-rust-sdk/pull/6339))
- Fix invited/knocked rooms disappearing from the room list after
join → leave/kick → re-invite when using Sliding Sync. The SDK now always
emits a room update so the room is surfaced correctly again.
@@ -25,6 +31,11 @@ All notable changes to this project will be documented in this file.
### Features
- Add support in the `MemoryStore`'s implementation of `EventCacheStore` for
having duplicate events in a room, where each duplicate is in a different
`LinkedChunk`. This is useful, e.g., when an event is in a room and a
thread in that room.
(#[6200](https://github.com/matrix-org/matrix-rust-sdk/pull/6200))
- Add `StateStore::upsert_thread_subscriptions()` method for bulk upserts.
([#5848](https://github.com/matrix-org/matrix-rust-sdk/pull/5848))
- The `LatestEventValue::LocalHasBeenSent` variant gains a new `event_id:
@@ -33,9 +44,33 @@ All notable changes to this project will be documented in this file.
- [**breaking**] `RelationalLinkedChunk::apply_updates` returns an error rather
than panicking. This is necessary in order to ensure certain behaviors are disallowed.
([#6061](https://github.com/matrix-org/matrix-rust-sdk/pull/6061))
- Add `RoomInfo::active_room_call_consensus_intent()` method to get the call intent for the current call,
based on what members are advertising.
([#6274](https://github.com/matrix-org/matrix-rust-sdk/pull/6274))
- Add `Room::is_call` to check for Call rooms (MSC3417)
([#6315](https://github.com/matrix-org/matrix-rust-sdk/pull/6315))
### Refactor
- [**breaking**] `TtlStoreValue` was moved and renamed to `matrix_sdk_common::ttl_cache::TtlValue`.
- [**breaking**] `Gap::prev_token` has been renamed to `Gap::token` since it's now used for both
the previous batch token and the next batch token.
([#6236](https://github.com/matrix-org/matrix-rust-sdk/pull/6236))
- [**breaking**] Invite acceptance details are no longer stored in `RoomInfo`,
and the accessors `RoomInfo.invite_acceptance_details()` and
`Room::invite_acceptance_details` have been removed. Instead, equivalent
details are stored in the Crypto store, and, provided the `e2e-encryption`
feature is enabled, are accessible via
`BaseClient::get_pending_key_bundle_details_for_room`.
([#6199](https://github.com/matrix-org/matrix-rust-sdk/pull/6199))
- [**breaking**] `once_cell` is no longer reexported from this crate. The types that were stabilized
in the Rust standard library can be used instead in most cases.
([#6194](https://github.com/matrix-org/matrix-rust-sdk/pull/6194))
- [**breaking**] All the `*StoreLock` structs use a `CrossProcessLockConfig` now instead of the previous `holder` value
and so does `StoreConfig` and `BaseClient::clone_with_in_memory_state_store. Passing a
`CrossProcessLockConfig::MultiProcess` will keep the same behaviour we had where the client uses the cross process
lock and using `CrossProcessLockConfig::SingleProcess` will disable the cross process lock.
([#6061](https://github.com/matrix-org/matrix-rust-sdk/pull/6061))
- [**breaking**] The `StateStore::upsert_thread_subscription` method has been removed in favor of a
bulk method `StateStore::upsert_thread_subscriptions`.
- [**breaking**] The `message-ids` feature has been removed. It was already a no-op and has now
@@ -48,7 +83,8 @@ All notable changes to this project will be documented in this file.
- Skip the serialization of custom join rules in the `RoomInfo` which prevented
the processing of sync responses containing events with custom join rules.
([#5924](https://github.com/matrix-org/matrix-rust-sdk/pull/5924)) (Low, [CVE-2025-66622](https://www.cve.org/CVERecord?id=CVE-2025-66622), [GHSA-jj6p-3m75-g2p3](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-jj6p-3m75-g2p3)).
([#5924](https://github.com/matrix-org/matrix-rust-sdk/pull/5924)) (
Low, [CVE-2025-66622](https://www.cve.org/CVERecord?id=CVE-2025-66622), [GHSA-jj6p-3m75-g2p3](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-jj6p-3m75-g2p3)).
### Refactor
@@ -58,8 +94,8 @@ All notable changes to this project will be documented in this file.
`maybe_decode()`. Its constructor has been removed since all its fields are
now public.
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
- `StateStoreData(Key/Value)::ServerInfo` has been split into the
`SupportedVersions` and `WellKnown` variants.
- `StateStoreData(Key/Value)::ServerInfo` has been split into the
`SupportedVersions` and `WellKnown` variants.
- [**breaking**] Upgrade Ruma to version 0.14.0.
([#5882](https://github.com/matrix-org/matrix-rust-sdk/pull/5882))
- `Client::sync_lock` has been renamed `Client::state_store_lock`.
@@ -80,11 +116,13 @@ All notable changes to this project will be documented in this file.
### Security Fixes
- Fix a panic in the `RoomMember::normalized_power_level` method.
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635)) (Low, [CVE-2025-59047](https://www.cve.org/CVERecord?id=CVE-2025-59047), [GHSA-qhj8-q5r6-8q6j](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-qhj8-q5r6-8q6j)).
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635)) (
Low, [CVE-2025-59047](https://www.cve.org/CVERecord?id=CVE-2025-59047), [GHSA-qhj8-q5r6-8q6j](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-qhj8-q5r6-8q6j)).
## [0.14.0] - 2025-09-04
### Features
- Add `SyncResponse::RoomUpdates::is_empty` to check if there were any room updates.
([#5593](https://github.com/matrix-org/matrix-rust-sdk/pull/5593))
- Add `EncryptionState::StateEncrypted` to represent rooms supporting encrypted
@@ -107,6 +145,7 @@ All notable changes to this project will be documented in this file.
([#5390](https://github.com/matrix-org/matrix-rust-sdk/pull/5390))
### Refactor
- [**breaking**] The `Stripped` variants of `RawAnySyncOrStrippedTimelineEvent`,
`RawAnySyncOrStrippedState` and `AnySyncOrStrippedState` use `StrippedState`
instead of `AnyStrippedStateEvent`.
@@ -153,6 +192,7 @@ All notable changes to this project will be documented in this file.
## [0.13.0] - 2025-07-10
### Features
- The `RoomInfo` now remembers when an invite was explicitly accepted when the
`BaseClient::room_joined()` method was called. A new getter for this
timestamp exists, the `RoomInfo::invite_accepted_at()` method returns this
@@ -190,9 +230,9 @@ No notable changes in this release.
- [**breaking**] The `MediaRetentionPolicy` can now trigger regular cleanups
with its new `cleanup_frequency` setting.
([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603))
- `Clone` is a supertrait of `EventCacheStoreMedia`.
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
- There are new `'static` bounds in `MediaService` for the media cache stores
- `Clone` is a supertrait of `EventCacheStoreMedia`.
- `EventCacheStoreMedia` has a new method `last_media_cleanup_time_inner`
- There are new `'static` bounds in `MediaService` for the media cache stores
- `event_cache::store::MemoryStore` implements `Clone`.
- `BaseClient` now has a `handle_verification_events` field which is `true` by
default and can be negated so the `NotificationClient` won't handle received
@@ -228,17 +268,17 @@ No notable changes in this release.
- [**breaking**] `EventCacheStore` allows to control which media content is
allowed in the media cache, and how long it should be kept, with a
`MediaRetentionPolicy`:
- `EventCacheStore::add_media_content()` has an extra argument,
`ignore_policy`, which decides whether a media content should ignore the
`MediaRetentionPolicy`. It should be stored alongside the media content.
- `EventCacheStore` has four new methods: `media_retention_policy()`,
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
`clean_up_media_cache()`.
- `EventCacheStore` implementations should delegate media cache methods to the
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
They need to implement the `EventCacheStoreMedia` trait that can be tested
with the `event_cache_store_media_integration_tests!` macro.
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
- `EventCacheStore::add_media_content()` has an extra argument,
`ignore_policy`, which decides whether a media content should ignore the
`MediaRetentionPolicy`. It should be stored alongside the media content.
- `EventCacheStore` has four new methods: `media_retention_policy()`,
`set_media_retention_policy()`, `set_ignore_media_retention_policy()` and
`clean_up_media_cache()`.
- `EventCacheStore` implementations should delegate media cache methods to the
methods of the same name of `MediaService` to use the `MediaRetentionPolicy`.
They need to implement the `EventCacheStoreMedia` trait that can be tested
with the `event_cache_store_media_integration_tests!` macro.
([#4571](https://github.com/matrix-org/matrix-rust-sdk/pull/4571))
### Refactor
@@ -278,8 +318,8 @@ No notable changes in this release.
- Use the `DisplayName` struct to protect against homoglyph attacks.
### Features
- Add `BaseClient::room_key_recipient_strategy` field
- `AmbiguityCache` contains the room member's user ID.
@@ -292,17 +332,17 @@ No notable changes in this release.
disambiguation.
- `Client::cross_process_store_locks_holder_name` is used everywhere:
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- `StoreConfig::new()` now takes a
`cross_process_store_locks_holder_name` argument.
- `StoreConfig` no longer implements `Default`.
- `BaseClient::new()` has been removed.
- `BaseClient::clone_with_in_memory_state_store()` now takes a
`cross_process_store_locks_holder_name` argument.
- `BaseClient` no longer implements `Default`.
- `EventCacheStoreLock::new()` no longer takes a `key` argument.
- `BuilderStoreConfig` no longer has
`cross_process_store_locks_holder_name` field for `Sqlite` and
`IndexedDb`.
- Make `ObservableMap::stream` works on `wasm32-unknown-unknown`.
@@ -312,8 +352,7 @@ No notable changes in this release.
by a custom one.
- Introduce a `DisplayName` struct which normalizes and sanitizes
display names.
display names.
### Refactor
@@ -353,34 +392,35 @@ display names.
other state events in `state` and `stripped_state`.
- `StateStore::get_user_ids` takes a `RoomMemberships` to be able to filter the results by any
membership state.
- `StateStore::get_joined_user_ids` and `StateStore::get_invited_user_ids` are deprecated.
- `StateStore::get_joined_user_ids` and `StateStore::get_invited_user_ids` are deprecated.
- `Room::members` takes a `RoomMemberships` to be able to filter the results by any membership
state.
- `Room::active_members` and `Room::joined_members` are deprecated.
- `Room::active_members` and `Room::joined_members` are deprecated.
- `RoomMember` has new methods:
- `can_ban`
- `can_invite`
- `can_kick`
- `can_redact`
- `can_send_message`
- `can_send_state`
- `can_trigger_room_notification`
- `can_ban`
- `can_invite`
- `can_kick`
- `can_redact`
- `can_send_message`
- `can_send_state`
- `can_trigger_room_notification`
- Move `StateStore::get_member_event` to `StateStoreExt`
- `StateStore::get_stripped_room_infos` is deprecated. All room infos should now be returned by
`get_room_infos`.
- `BaseClient::get_stripped_rooms` is deprecated. Use `get_rooms_filtered` with
`RoomStateFilter::INVITED` instead.
- Add methods to `StateStore` to be able to retrieve data in batch
- `get_state_events_for_keys`
- `get_profiles`
- `get_presence_events`
- `get_users_with_display_names`
- `get_state_events_for_keys`
- `get_profiles`
- `get_presence_events`
- `get_users_with_display_names`
- Move `Session`, `SessionTokens` and associated methods to the `matrix-sdk` crate.
- Add `Room::subscribe_info`
# 0.5.1
## Bug Fixes
- #664: Fix regression with push rules being applied to the own user_id only instead of all but the own user_id
# 0.5.0
+7 -1
View File
@@ -40,6 +40,12 @@ experimental-encrypted-state-events = [
"matrix-sdk-crypto?/experimental-encrypted-state-events"
]
# Enable experimental support for pushing secrets
experimental-push-secrets = [
"e2e-encryption",
"matrix-sdk-crypto?/experimental-push-secrets"
]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
# Private feature, see
@@ -80,7 +86,6 @@ matrix-sdk-common.workspace = true
matrix-sdk-crypto = { workspace = true, optional = true }
matrix-sdk-store-encryption.workspace = true
matrix-sdk-test = { workspace = true, optional = true }
once_cell.workspace = true
regex.workspace = true
ruma = { workspace = true, features = [
"unstable-msc2867",
@@ -114,6 +119,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[target.'cfg(target_family = "wasm")'.dev-dependencies]
wasm-bindgen-test.workspace = true
getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] }
gloo-timers = { workspace = true, features = ["futures"] }
[lints]
+150 -88
View File
@@ -24,18 +24,19 @@ use std::{
use eyeball::{SharedObservable, Subscriber};
use eyeball_im::{Vector, VectorDiff};
use futures_util::Stream;
use matrix_sdk_common::timer;
use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, timer};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{
CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
TrustRequirement, store::DynCryptoStore, types::requests::ToDeviceRequest,
TrustRequirement, store::DynCryptoStore, store::types::RoomPendingKeyBundleDetails,
types::requests::ToDeviceRequest,
};
#[cfg(doc)]
use ruma::DeviceId;
#[cfg(feature = "e2e-encryption")]
use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedUserId, RoomId, UserId,
OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{self as api, sync::sync_events::v5},
events::{
StateEvent, StateEventType,
@@ -54,7 +55,7 @@ use tracing::{Level, debug, enabled, info, instrument, warn};
#[cfg(feature = "e2e-encryption")]
use crate::RoomMemberships;
use crate::{
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
RoomStateFilter, SessionMeta,
deserialized_responses::DisplayName,
error::{Error, Result},
event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
@@ -79,9 +80,12 @@ use crate::{
///
/// ```rust
/// use matrix_sdk_base::{BaseClient, ThreadingSupport, store::StoreConfig};
/// use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
///
/// let client = BaseClient::new(
/// StoreConfig::new("cross-process-holder-name".to_owned()),
/// StoreConfig::new(CrossProcessLockConfig::multi_process(
/// "cross-process-holder-name".to_owned(),
/// )),
/// ThreadingSupport::Disabled,
/// );
/// ```
@@ -201,11 +205,10 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
pub async fn clone_with_in_memory_state_store(
&self,
cross_process_store_locks_holder_name: &str,
cross_process_mode: CrossProcessLockConfig,
handle_verification_events: bool,
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder_name.to_owned())
.state_store(MemoryStore::new());
let config = StoreConfig::new(cross_process_mode).state_store(MemoryStore::new());
let config = config.crypto_store(self.crypto_store.clone());
let copy = Self {
@@ -238,11 +241,10 @@ impl BaseClient {
#[allow(clippy::unused_async)]
pub async fn clone_with_in_memory_state_store(
&self,
cross_process_store_locks_holder: &str,
cross_process_store_config: CrossProcessLockConfig,
_handle_verification_events: bool,
) -> Result<Self> {
let config = StoreConfig::new(cross_process_store_locks_holder.to_owned())
.state_store(MemoryStore::new());
let config = StoreConfig::new(cross_process_store_config).state_store(MemoryStore::new());
Ok(Self::new(config, ThreadingSupport::Disabled))
}
@@ -397,6 +399,14 @@ impl BaseClient {
room_info.mark_as_knocked();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
// We are no longer joined to the room, so the invite acceptance details are no
// longer relevant.
#[cfg(feature = "e2e-encryption")]
if let Some(olm_machine) = self.olm_machine().await.as_ref() {
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
}
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.state_store.save_changes(&changes).await?; // Update the store
@@ -425,15 +435,16 @@ impl BaseClient {
/// * `inviter` - When joining this room in response to an invitation, the
/// inviter should be recorded before sending the join request to the
/// server. Providing the inviter here ensures that the
/// [`InviteAcceptanceDetails`] are stored for this room.
/// [`RoomPendingKeyBundleDetails`] are stored for this room.
///
/// # Examples
///
/// ```rust
/// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport};
/// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
/// use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
/// # async {
/// # let client = BaseClient::new(StoreConfig::new("example".to_owned()), ThreadingSupport::Disabled);
/// # let client = BaseClient::new(StoreConfig::new(CrossProcessLockConfig::multi_process("example")), ThreadingSupport::Disabled);
/// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
/// # async fn maybe_get_inviter(room_id: &RoomId) -> anyhow::Result<Option<OwnedUserId>> { todo!() }
/// # let room_id: &RoomId = todo!();
@@ -457,30 +468,35 @@ impl BaseClient {
let _state_store_lock = self.state_store_lock().lock().await;
let mut room_info = room.clone_info();
let previous_state = room.state();
room_info.mark_as_joined();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
// If our previous state was an invite and we're now in the joined state, this
// means that the user has explicitly accepted an invite. Let's
// remember some details about the invite.
//
// This is somewhat of a workaround for our lack of cryptographic membership.
// Later on we will decide if historic room keys should be accepted
// based on this info. If a user has accepted an invite and we receive a room
// key bundle shortly after, we might accept it. If we don't do
// this, the homeserver could trick us into accepting any historic room key
// bundle.
if previous_state == RoomState::Invited
&& let Some(inviter) = inviter
#[cfg(feature = "e2e-encryption")]
{
let details = InviteAcceptanceDetails {
invite_accepted_at: MilliSecondsSinceUnixEpoch::now(),
inviter,
};
room_info.set_invite_acceptance_details(details);
// If our previous state was an invite and we're now in the joined state, this
// means that the user has explicitly accepted an invite. Let's
// remember some details about the invite.
//
// This is somewhat of a workaround for our lack of cryptographic membership.
// Later on we will decide if historic room keys should be accepted
// based on this info. If a user has accepted an invite and we receive a room
// key bundle shortly after, we might accept it. If we don't do
// this, the homeserver could trick us into accepting any historic room key
// bundle.
let previous_state = room.state();
if previous_state == RoomState::Invited
&& let Some(inviter) = inviter
&& let Some(olm_machine) = self.olm_machine().await.as_ref()
{
olm_machine.store().store_room_pending_key_bundle(room_id, &inviter).await?
}
}
#[cfg(not(feature = "e2e-encryption"))]
{
// suppress unused argument warning
let _ = inviter;
}
let mut changes = StateChanges::default();
@@ -507,6 +523,14 @@ impl BaseClient {
room_info.mark_as_left();
room_info.mark_state_partially_synced();
room_info.mark_members_missing(); // the own member event changed
// We are no longer joined to the room, so the invite acceptance details are no
// longer relevant.
#[cfg(feature = "e2e-encryption")]
if let Some(olm_machine) = self.olm_machine().await.as_ref() {
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
}
let mut changes = StateChanges::default();
changes.add_room(room_info.clone());
self.state_store.save_changes(&changes).await?; // Update the store
@@ -625,6 +649,13 @@ impl BaseClient {
let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
BTreeMap::new();
#[cfg(feature = "e2e-encryption")]
let e2ee_context = processors::e2ee::E2EE::new(
olm_machine.as_ref(),
&self.decryption_settings,
self.handle_verification_events,
);
for (room_id, joined_room) in response.rooms.join {
let joined_room_update = processors::room::sync_v2::update_joined_room(
&mut context,
@@ -641,11 +672,7 @@ impl BaseClient {
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
&self.decryption_settings,
self.handle_verification_events,
),
&e2ee_context,
)
.await?;
@@ -667,11 +694,7 @@ impl BaseClient {
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
&self.decryption_settings,
self.handle_verification_events,
),
&e2ee_context,
)
.await?;
@@ -689,6 +712,8 @@ impl BaseClient {
&mut notifications,
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
&e2ee_context,
)
.await?;
@@ -706,6 +731,8 @@ impl BaseClient {
&mut notifications,
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
&e2ee_context,
)
.await?;
@@ -893,6 +920,18 @@ impl BaseClient {
let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
#[cfg(feature = "e2e-encryption")]
if let Some(olm) = self.olm_machine().await.as_ref() {
// With the introduction of MSC4268, it is no longer sufficient to check for
// changes to session recipients when we send a message, since we may miss
// join/leave pairs in our view of the room state. Instead, we should rotate
// the room key whenever we fully reload the member list as a precaution.
tracing::debug!("Rotating room key due to full member list reload");
if let Err(e) = olm.discard_room_key(room_id).await {
tracing::warn!("Error discarding room key: {e:?}");
}
}
Ok(())
}
@@ -970,11 +1009,13 @@ impl BaseClient {
let members = self.state_store.get_user_ids(room_id, filter).await?;
let settings = EncryptionSettings::new(
let Some(settings) = EncryptionSettings::from_possibly_redacted(
room_encryption_event,
history_visibility,
self.room_key_recipient_strategy.clone(),
);
) else {
return Err(Error::EncryptionNotEnabled);
};
Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
}
@@ -1084,6 +1125,24 @@ impl BaseClient {
}
}
}
/// Check the record of whether we are waiting for an [MSC4268] key bundle
/// for the given room.
///
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
#[cfg(feature = "e2e-encryption")]
pub async fn get_pending_key_bundle_details_for_room(
&self,
room_id: &RoomId,
) -> Result<Option<RoomPendingKeyBundleDetails>> {
let result = match self.olm_machine().await.as_ref() {
Some(machine) => {
machine.store().get_pending_key_bundle_details_for_room(room_id).await?
}
None => None,
};
Ok(result)
}
}
/// Represent the `required_state` values sent by a sync request.
@@ -1147,11 +1206,14 @@ impl From<&v5::Request> for RequestedRequiredStates {
mod tests {
use std::collections::HashMap;
use assert_matches2::{assert_let, assert_matches};
use assert_matches2::assert_let;
#[cfg(feature = "e2e-encryption")]
use assert_matches2::assert_matches;
use futures_util::FutureExt as _;
use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
use matrix_sdk_test::{
BOB, InvitedRoomBuilder, LeftRoomBuilder, StateTestEvent, StrippedStateTestEvent,
SyncResponseBuilder, async_test, event_factory::EventFactory, ruma_response_from_json,
BOB, InvitedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, async_test,
event_factory::EventFactory, ruma_response_from_json,
};
use ruma::{
api::client::{self as api, sync::sync_events::v5},
@@ -1331,6 +1393,7 @@ mod tests {
let room_id = room_id!("!test:example.org");
let client = logged_in_base_client(Some(user_id)).await;
let f = EventFactory::new();
let mut sync_builder = SyncResponseBuilder::new();
@@ -1349,19 +1412,14 @@ mod tests {
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
let response = sync_builder
.add_invited_room(InvitedRoomBuilder::new(room_id).add_state_event(
StrippedStateTestEvent::Custom(json!({
"content": {
"displayname": "Alice",
"membership": "invite",
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 1432735824653u64,
"sender": "@example:example.org",
"state_key": user_id,
"type": "m.room.member",
})),
))
.add_invited_room(
InvitedRoomBuilder::new(room_id).add_state_event(
f.member(user_id)
.sender(user_id!("@example:example.org"))
.membership(MembershipState::Invite)
.display_name("Alice"),
),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
@@ -1461,7 +1519,7 @@ mod tests {
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
ThreadingSupport::Disabled,
);
client
@@ -1523,7 +1581,7 @@ mod tests {
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
ThreadingSupport::Disabled,
);
client
@@ -1587,7 +1645,7 @@ mod tests {
let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
ThreadingSupport::Disabled,
);
client
@@ -1601,23 +1659,13 @@ mod tests {
.unwrap();
// Preamble: let the SDK know about the room, and that the invited user left it.
let f = EventFactory::new().sender(user_id);
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_state_event(
StateTestEvent::Custom(json!({
"content": {
"avatar_url": null,
"displayname": null,
"membership": "leave"
},
"event_id": "$151803140217rkvjc:localhost",
"origin_server_ts": 151800139,
"room_id": room_id,
"sender": user_id,
"state_key": user_id,
"type": "m.room.member",
})),
))
.add_joined_room(
matrix_sdk_test::JoinedRoomBuilder::new(room_id)
.add_state_event(f.member(user_id).leave()),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
@@ -1661,7 +1709,7 @@ mod tests {
async fn test_ignored_user_list_changes() {
let user_id = user_id!("@alice:example.org");
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
StoreConfig::new(CrossProcessLockConfig::SingleProcess),
ThreadingSupport::Disabled,
);
@@ -1721,37 +1769,43 @@ mod tests {
assert!(client.is_user_ignored(ignored_user_id).await);
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_invite_details_are_set() {
let user_id = user_id!("@alice:localhost");
let client = logged_in_base_client(Some(user_id)).await;
let invited_room_id = room_id!("!invited:localhost");
let known_room_id = room_id!("!invited:localhost");
let unknown_room_id = room_id!("!unknown:localhost");
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_invited_room(InvitedRoomBuilder::new(invited_room_id))
.add_invited_room(InvitedRoomBuilder::new(known_room_id))
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
// Let us first check the initial state, we should have a room in the invite
// state.
let invited_room = client
.get_room(invited_room_id)
.get_room(known_room_id)
.expect("The sync should have created a room in the invited state");
assert_eq!(invited_room.state(), RoomState::Invited);
assert!(invited_room.invite_acceptance_details().is_none());
assert!(
client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
);
// Now we join the room.
let joined_room = client
.room_joined(invited_room_id, Some(user_id.to_owned()))
.room_joined(known_room_id, Some(user_id.to_owned()))
.await
.expect("We should be able to mark a room as joined");
// Yup, we now have some invite details.
assert_eq!(joined_room.state(), RoomState::Joined);
assert_matches!(joined_room.invite_acceptance_details(), Some(details));
assert_matches!(
client.get_pending_key_bundle_details_for_room(known_room_id).await,
Ok(Some(details))
);
assert_eq!(details.inviter, user_id);
// If we didn't know about the room before the join, we assume that there wasn't
@@ -1763,19 +1817,27 @@ mod tests {
.expect("We should be able to mark a room as joined");
assert_eq!(unknown_room.state(), RoomState::Joined);
assert!(unknown_room.invite_acceptance_details().is_none());
assert!(
client
.get_pending_key_bundle_details_for_room(unknown_room_id)
.await
.unwrap()
.is_none()
);
sync_builder.clear();
let response =
sync_builder.add_left_room(LeftRoomBuilder::new(invited_room_id)).build_sync_response();
sync_builder.add_left_room(LeftRoomBuilder::new(known_room_id)).build_sync_response();
client.receive_sync_response(response).await.unwrap();
// Now that we left the room, we shouldn't have any details anymore.
let left_room = client
.get_room(invited_room_id)
.get_room(known_room_id)
.expect("The sync should have created a room in the invited state");
assert_eq!(left_room.state(), RoomState::Left);
assert!(left_room.invite_acceptance_details().is_none());
assert!(
client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
);
}
}
@@ -14,10 +14,9 @@
//! SDK-specific variations of response types from Ruma.
use std::{collections::BTreeMap, fmt, hash::Hash, iter};
use std::{collections::BTreeMap, fmt, hash::Hash, iter, sync::LazyLock};
pub use matrix_sdk_common::deserialized_responses::*;
use once_cell::sync::Lazy;
use regex::Regex;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, UInt,
@@ -72,15 +71,15 @@ pub struct AmbiguityChanges {
pub changes: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, AmbiguityChange>>,
}
static MXID_REGEX: Lazy<Regex> = Lazy::new(|| {
static MXID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(DisplayName::MXID_PATTERN)
.expect("We should be able to create a regex from our static MXID pattern")
});
static LEFT_TO_RIGHT_REGEX: Lazy<Regex> = Lazy::new(|| {
static LEFT_TO_RIGHT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(DisplayName::LEFT_TO_RIGHT_PATTERN)
.expect("We should be able to create a regex from our static left-to-right pattern")
});
static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
static HIDDEN_CHARACTERS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(DisplayName::HIDDEN_CHARACTERS_PATTERN)
.expect("We should be able to create a regex from our static hidden characters pattern")
});
@@ -89,7 +88,7 @@ static HIDDEN_CHARACTERS_REGEX: Lazy<Regex> = Lazy::new(|| {
///
/// This is used to replace an `i` with a lowercase `l`, i.e. to mark "Hello"
/// and "HeIlo" as ambiguous. Decancer will lowercase an `I` for us.
static I_REGEX: Lazy<Regex> = Lazy::new(|| {
static I_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("[i]").expect("We should be able to create a regex from our uppercase I pattern")
});
@@ -97,7 +96,7 @@ static I_REGEX: Lazy<Regex> = Lazy::new(|| {
///
/// This is used to replace an `0` with a lowercase `o`, i.e. to mark "HellO"
/// and "Hell0" as ambiguous. Decancer will lowercase an `O` for us.
static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
static ZERO_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("[0]").expect("We should be able to create a regex from our zero pattern")
});
@@ -105,7 +104,7 @@ static ZERO_REGEX: Lazy<Regex> = Lazy::new(|| {
///
/// This is used to replace a `.` with a `:`, i.e. to mark "@mxid.domain.tld" as
/// ambiguous.
static DOT_REGEX: Lazy<Regex> = Lazy::new(|| {
static DOT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("[.\u{1d16d}]").expect("We should be able to create a regex from our dot pattern")
});
@@ -24,7 +24,9 @@ pub type Event = TimelineEvent;
/// The kind of gap the event storage holds.
#[derive(Clone, Debug)]
pub struct Gap {
/// The token to use in the query, extracted from a previous "from" /
/// "end" field of a `/messages` response.
pub prev_token: String,
/// The token to use in the pagination query as the `from` parameter,
/// extracted from a previous `start` / `end` field of a `/messages`
/// response, or from the `prev_batch` / `next_batch` field of a `/sync`
/// response.
pub token: String,
}
@@ -141,6 +141,10 @@ pub trait EventCacheStoreIntegrationTests {
/// already exist in the store.
async fn test_linked_chunk_exists_before_referenced(&self);
/// Test that the same event can exist in a room's linked chunk and a
/// thread's linked chunk simultaneously.
async fn test_linked_chunk_allows_same_event_in_room_and_thread(&self);
/// Test loading the last chunk in a linked chunk from the store.
async fn test_load_last_chunk(&self);
@@ -202,18 +206,34 @@ pub trait EventCacheStoreIntegrationTests {
/// Test that an event can be found or not.
async fn test_find_event(&self);
/// Test that an event can be found when it exists in both a room and a
/// thread in that room.
async fn test_find_event_when_event_in_room_and_thread(&self);
/// Test that finding event relations works as expected.
async fn test_find_event_relations(&self);
/// Test that find event relations works as expected when an event is both a
/// room and a thread in that room.
async fn test_find_event_relations_when_event_in_room_and_thread(&self);
/// Test that getting all events in a room works as expected.
async fn test_get_room_events(&self);
/// Test that getting events in a room of a certain type works as expected.
async fn test_get_room_events_filtered(&self);
/// Test that getting all events in a room works as expected when the event
/// is in both a room and thread in that room.
async fn test_get_room_events_with_event_in_room_and_thread(&self);
/// Test that saving an event works as expected.
async fn test_save_event(&self);
/// Test that saving an existing event updates it's contents in both room
/// and thread linked chunks.
async fn test_save_event_updates_event_in_room_and_thread(&self);
/// Test multiple things related to distinguishing a thread linked chunk
/// from a room linked chunk.
async fn test_thread_vs_room_linked_chunk(&self);
@@ -242,7 +262,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "parmesan".to_owned() },
gap: Gap { token: "parmesan".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
@@ -283,7 +303,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_eq!(second.identifier(), CId::new(1));
assert_matches!(second.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "parmesan");
assert_eq!(gap.token, "parmesan");
});
}
@@ -335,7 +355,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(41)),
new: CId::new(42),
next: None,
gap: Gap { prev_token: "gap".to_owned() },
gap: Gap { token: "gap".to_owned() },
}],
)
.await
@@ -348,13 +368,69 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: None,
new: CId::new(42),
next: Some(CId::new(43)),
gap: Gap { prev_token: "gap".to_owned() },
gap: Gap { token: "gap".to_owned() },
}],
)
.await
.unwrap_err();
}
async fn test_linked_chunk_allows_same_event_in_room_and_thread(&self) {
// This test verifies that the same event can appear in both a room's linked
// chunk and a thread's linked chunk. This is the real-world use case:
// a thread reply appears in both the main room timeline and the thread.
let room_id = *DEFAULT_TEST_ROOM_ID;
let thread_root = event_id!("$thread_root");
// Create an event that will be inserted into both the room and thread linked
// chunks.
let event_id = event_id!("$thread_reply");
let event = make_test_event_with_event_id(room_id, "thread reply", Some(event_id));
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
// Insert the event into the room's linked chunk.
self.handle_linked_chunk_updates(
room_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event.clone()] },
],
)
.await
.unwrap();
// Insert the same event into the thread's linked chunk.
self.handle_linked_chunk_updates(
thread_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event] },
],
)
.await
.unwrap();
// Verify both entries exist by loading chunks from both linked chunk IDs.
let room_chunks = self.load_all_chunks(room_linked_chunk_id).await.unwrap();
let thread_chunks = self.load_all_chunks(thread_linked_chunk_id).await.unwrap();
assert_eq!(room_chunks.len(), 1);
assert_eq!(thread_chunks.len(), 1);
// Verify the event is in both.
assert_matches!(&room_chunks[0].content, ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
});
assert_matches!(&thread_chunks[0].content, ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
});
}
async fn test_load_all_chunks_metadata(&self) {
let room_id = room_id!("!r0:matrix.org");
let linked_chunk_id = LinkedChunkId::Room(room_id);
@@ -377,7 +453,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "parmesan".to_owned() },
gap: Gap { token: "parmesan".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
@@ -626,7 +702,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "morbier".to_owned() },
gap: Gap { token: "morbier".to_owned() },
},
// new chunk for items
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
@@ -704,7 +780,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_eq!(chunk.lazy_previous(), Some(CId::new(0)));
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "morbier");
assert_eq!(gap.token, "morbier");
});
});
@@ -742,7 +818,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
// Already asserted, but let's be sure nothing breaks.
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "morbier");
assert_eq!(gap.token, "morbier");
});
});
@@ -793,7 +869,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert!(chunk.lazy_previous().is_none());
assert_matches!(chunk.content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "morbier");
assert_eq!(gap.token, "morbier");
});
});
@@ -825,19 +901,19 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: None,
new: CId::new(42),
next: None,
gap: Gap { prev_token: "raclette".to_owned() },
gap: Gap { token: "raclette".to_owned() },
},
Update::NewGapChunk {
previous: Some(CId::new(42)),
new: CId::new(43),
next: None,
gap: Gap { prev_token: "fondue".to_owned() },
gap: Gap { token: "fondue".to_owned() },
},
Update::NewGapChunk {
previous: Some(CId::new(43)),
new: CId::new(44),
next: None,
gap: Gap { prev_token: "tartiflette".to_owned() },
gap: Gap { token: "tartiflette".to_owned() },
},
Update::RemoveChunk(CId::new(43)),
],
@@ -855,7 +931,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_eq!(c.previous, None);
assert_eq!(c.next, Some(CId::new(44)));
assert_matches!(c.content, ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "raclette");
assert_eq!(gap.token, "raclette");
});
let c = chunks.remove(0);
@@ -863,7 +939,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_eq!(c.previous, Some(CId::new(42)));
assert_eq!(c.next, None);
assert_matches!(c.content, ChunkContent::Gap(gap) => {
assert_eq!(gap.prev_token, "tartiflette");
assert_eq!(gap.token, "tartiflette");
});
}
@@ -1043,7 +1119,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(42)),
new: CId::new(54),
next: None,
gap: Gap { prev_token: "fondue".to_owned() },
gap: Gap { token: "fondue".to_owned() },
},
Update::PushItems {
at: Position::new(CId::new(42), 0),
@@ -1074,7 +1150,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(42)),
new: CId::new(54),
next: None,
gap: Gap { prev_token: "fondue".to_owned() },
gap: Gap { token: "fondue".to_owned() },
},
Update::PushItems {
at: Position::new(CId::new(42), 0),
@@ -1203,7 +1279,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "bleu d'auvergne".to_owned() },
gap: Gap { token: "bleu d'auvergne".to_owned() },
},
// another items chunk
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
@@ -1330,7 +1406,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
previous: Some(CId::new(0)),
new: CId::new(1),
next: None,
gap: Gap { prev_token: "brillat-savarin".to_owned() },
gap: Gap { token: "brillat-savarin".to_owned() },
},
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },
Update::PushItems {
@@ -1361,12 +1437,12 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
self.filter_duplicated_events(
linked_chunk_id,
vec![
event_comte.event_id().unwrap().to_owned(),
event_raclette.event_id().unwrap().to_owned(),
event_morbier.event_id().unwrap().to_owned(),
event_gruyere.event_id().unwrap().to_owned(),
event_tome.event_id().unwrap().to_owned(),
event_mont_dor.event_id().unwrap().to_owned(),
event_comte.event_id().unwrap(),
event_raclette.event_id().unwrap(),
event_morbier.event_id().unwrap(),
event_gruyere.event_id().unwrap(),
event_tome.event_id().unwrap(),
event_mont_dor.event_id().unwrap(),
],
)
.await
@@ -1460,6 +1536,75 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
);
}
async fn test_find_event_when_event_in_room_and_thread(&self) {
let room_id = *DEFAULT_TEST_ROOM_ID;
let thread_root = event_id!("$thread_root");
// Create an event that will be only be inserted into the room
let room_event_id = event_id!("$room_event");
let room_event = make_test_event_with_event_id(room_id, "room event", Some(room_event_id));
// Create an event that will only be inserted into the thread
let thread_event_id = event_id!("$thread_event");
let thread_event =
make_test_event_with_event_id(room_id, "thread event", Some(thread_event_id));
// Create an event that will be inserted into both the room and thread linked
// chunks.
let room_and_thread_event_id = event_id!("$room_and_thread");
let room_and_thread_event = make_test_event_with_event_id(
room_id,
"room and thread",
Some(room_and_thread_event_id),
);
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
// Insert the relevant events into the room's linked chunk.
self.handle_linked_chunk_updates(
room_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(1), 0),
items: vec![room_event, room_and_thread_event.clone()],
},
],
)
.await
.unwrap();
// Insert the relevant events into the thread's linked chunk.
self.handle_linked_chunk_updates(
thread_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(1), 0),
items: vec![thread_event, room_and_thread_event],
},
],
)
.await
.unwrap();
// Verify that event that is only in the room can be retrieved
assert_matches!(self.find_event(room_id, room_event_id).await, Ok(Some(event)) => {
assert_eq!(event.event_id().unwrap(), room_event_id)
});
// Verify that the event that is only in the thread can be retrieved
assert_matches!(self.find_event(room_id, thread_event_id).await, Ok(Some(event)) => {
assert_eq!(event.event_id().unwrap(), thread_event_id)
});
// Verify that event that is in both room and thread can be retrieved
assert_matches!(self.find_event(room_id, room_and_thread_event_id).await, Ok(Some(event)) => {
assert_eq!(event.event_id().unwrap(), room_and_thread_event_id);
});
}
async fn test_find_event_relations(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
@@ -1574,6 +1719,121 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
);
}
async fn test_find_event_relations_when_event_in_room_and_thread(&self) {
let room_id = *DEFAULT_TEST_ROOM_ID;
let thread_root = event_id!("$thread_root");
// Create an event that will inserted into both the room and thread linked
// chunks.
let event_id = event_id!("$event");
let event = make_test_event_with_event_id(room_id, "event", Some(event_id));
// Create an event that will only be inserted into the thread in order to help
// distinguish between the room and thread linked chunks.
let extra_thread_event_id = event_id!("$extra_thread_event");
let extra_thread_event = make_test_event_with_event_id(
room_id,
"extra thread event",
Some(extra_thread_event_id),
);
// Create a reaction that will only be inserted into the room
let room_reaction_id = event_id!("$room_reaction");
let room_reaction = EventFactory::new()
.room(room_id)
.sender(*ALICE)
.reaction(event_id, "room")
.event_id(room_reaction_id)
.into_event();
// Create a reaction that will only be inserted into the thread
let thread_reaction_id = event_id!("$thread_reaction");
let thread_reaction = EventFactory::new()
.room(room_id)
.sender(*ALICE)
.reaction(event_id, "thread")
.event_id(thread_reaction_id)
.into_event();
// Create a reaction that will be inserted into both the room and thread linked
// chunks.
let room_and_thread_reaction_id = event_id!("$room_and_thread_reaction");
let room_and_thread_reaction = EventFactory::new()
.room(room_id)
.sender(*ALICE)
.reaction(event_id, "room and thread")
.event_id(room_and_thread_reaction_id)
.into_event();
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
// Insert the relevant events into the room's linked chunk.
self.handle_linked_chunk_updates(
room_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(1), 0),
items: vec![event.clone(), room_reaction, room_and_thread_reaction.clone()],
},
],
)
.await
.unwrap();
// Insert the relevant events into the thread's linked chunk.
self.handle_linked_chunk_updates(
thread_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(1), 0),
items: vec![
event.clone(),
extra_thread_event,
thread_reaction,
room_and_thread_reaction,
],
},
],
)
.await
.unwrap();
// Verify that only related events from the room are returned
assert_matches!(self.find_event_relations(room_id, event_id, None).await, Ok(relations) => {
assert_eq!(relations.len(), 3);
// Verify that room reaction is in the list and associated with its
// position in the room linked chunk.
let room_relation = relations
.iter()
.find(|relation| relation.0.event_id().unwrap() == room_reaction_id)
.unwrap();
assert_matches!(room_relation, (_, Some(position)) => {
assert_eq!(*position, Position::new(CId::new(1), 1));
});
// Verify that thread reaction is in the list and not associated with a
// position, as all positions are provided for the room linked chunk.
let thread_relation = relations
.iter()
.find(|relation| relation.0.event_id().unwrap() == thread_reaction_id)
.unwrap();
assert_matches!(thread_relation, (_, None));
// Verify that room and thread reaction is in the list and associated
// with its position in the room linked chunk, not the thread linked chunk.
let room_and_thread_relation = relations
.iter()
.find(|relation| relation.0.event_id().unwrap() == room_and_thread_reaction_id)
.unwrap();
assert_matches!(room_and_thread_relation, (_, Some(position)) => {
assert_eq!(*position, Position::new(CId::new(1), 2));
});
});
}
async fn test_get_room_events(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
@@ -1702,6 +1962,73 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
assert_expected_events!(events, [first_event]);
}
async fn test_get_room_events_with_event_in_room_and_thread(&self) {
let room_id = *DEFAULT_TEST_ROOM_ID;
let thread_root = event_id!("$thread_root");
// Create an event that will be only be inserted into the room
let room_event_id = event_id!("$room_event");
let room_event = make_test_event_with_event_id(room_id, "room event", Some(room_event_id));
// Create an event that will only be inserted into the thread. This may not be a
// sensible operation in practice, as threads seem to always exist in a
// room, but let's test it anyway.
let thread_event_id = event_id!("$thread_event");
let thread_event =
make_test_event_with_event_id(room_id, "thread event", Some(thread_event_id));
// Create an event that will be inserted into both the room and thread linked
// chunks.
let room_and_thread_event_id = event_id!("$room_and_thread");
let room_and_thread_event = make_test_event_with_event_id(
room_id,
"room and thread",
Some(room_and_thread_event_id),
);
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
// Insert the relevant events into the room's linked chunk.
self.handle_linked_chunk_updates(
room_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(1), 0),
items: vec![room_event, room_and_thread_event.clone()],
},
],
)
.await
.unwrap();
// Insert the relevant events into the thread's linked chunk.
self.handle_linked_chunk_updates(
thread_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems {
at: Position::new(CId::new(1), 0),
items: vec![thread_event, room_and_thread_event],
},
],
)
.await
.unwrap();
// Verify that all events can be retrieved and none are duplicated in the
// returned list.
let expected_event_ids =
BTreeSet::from([room_event_id, thread_event_id, room_and_thread_event_id]);
assert_matches!(self.get_room_events(room_id, None, None).await, Ok(events) => {
assert_eq!(events.len(), 3);
assert!(events.iter().all(|event| {
expected_event_ids.contains(&event.event_id().unwrap().as_ref())
}));
});
}
async fn test_save_event(&self) {
let room_id = room_id!("!r0:matrix.org");
let another_room_id = room_id!("!r1:matrix.org");
@@ -1746,6 +2073,66 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
);
}
async fn test_save_event_updates_event_in_room_and_thread(&self) {
let room_id = *DEFAULT_TEST_ROOM_ID;
let thread_root = event_id!("$thread_root");
// Create an event that will be inserted into both the room and thread linked
// chunks.
let event_id = event_id!("$event");
let event = make_test_event_with_event_id(room_id, "event", Some(event_id));
let room_linked_chunk_id = LinkedChunkId::Room(room_id);
let thread_linked_chunk_id = LinkedChunkId::Thread(room_id, thread_root);
// Insert the relevant events into the room's linked chunk.
self.handle_linked_chunk_updates(
room_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event.clone()] },
],
)
.await
.unwrap();
// Insert the relevant events into the thread's linked chunk.
self.handle_linked_chunk_updates(
thread_linked_chunk_id,
vec![
Update::NewItemsChunk { previous: None, new: CId::new(1), next: None },
Update::PushItems { at: Position::new(CId::new(1), 0), items: vec![event.clone()] },
],
)
.await
.unwrap();
// Save updated version of original event, which should replace the content of
// the existing event
let updated_content = "updated content";
let updated = make_test_event_with_event_id(room_id, updated_content, Some(event_id));
self.save_event(room_id, updated).await.unwrap();
// Load all chunks from both room and thread
let room_chunks = self.load_all_chunks(room_linked_chunk_id).await.unwrap();
let thread_chunks = self.load_all_chunks(thread_linked_chunk_id).await.unwrap();
assert_eq!(room_chunks.len(), 1);
assert_eq!(thread_chunks.len(), 1);
// Verify the event has been updated in both room and thread
assert_matches!(&room_chunks[0].content, ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
check_test_event(&events[0], updated_content);
});
assert_matches!(&thread_chunks[0].content, ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id().as_deref(), Some(event_id));
check_test_event(&events[0], updated_content);
});
}
async fn test_thread_vs_room_linked_chunk(&self) {
let room_id = room_id!("!r0:matrix.org");
@@ -1826,10 +2213,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
let dups = self
.filter_duplicated_events(
LinkedChunkId::Thread(room_id, thread_root1.event_id().unwrap().as_ref()),
vec![
thread1_ev.event_id().unwrap().to_owned(),
room_ev.event_id().unwrap().to_owned(),
],
vec![thread1_ev.event_id().unwrap(), room_ev.event_id().unwrap()],
)
.await
.unwrap();
@@ -1933,6 +2317,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_linked_chunk_exists_before_referenced().await;
}
#[async_test]
async fn test_linked_chunk_allow_same_event_in_room_and_thread() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_linked_chunk_allows_same_event_in_room_and_thread().await;
}
#[async_test]
async fn test_load_last_chunk() {
let event_cache_store =
@@ -2066,6 +2457,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_find_event().await;
}
#[async_test]
async fn test_find_event_when_event_in_room_and_thread() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_find_event_when_event_in_room_and_thread().await;
}
#[async_test]
async fn test_find_event_relations() {
let event_cache_store =
@@ -2073,6 +2471,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_find_event_relations().await;
}
#[async_test]
async fn test_find_event_relations_when_event_in_room_and_thread() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_find_event_relations_when_event_in_room_and_thread().await;
}
#[async_test]
async fn test_get_room_events() {
let event_cache_store =
@@ -2087,6 +2492,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_get_room_events_filtered().await;
}
#[async_test]
async fn test_get_room_events_with_event_in_room_and_thread() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_get_room_events_with_event_in_room_and_thread().await;
}
#[async_test]
async fn test_save_event() {
let event_cache_store =
@@ -2094,6 +2506,13 @@ macro_rules! event_cache_store_integration_tests {
event_cache_store.test_save_event().await;
}
#[async_test]
async fn test_save_event_updates_event_in_room_and_thread() {
let event_cache_store =
get_event_cache_store().await.unwrap().into_event_cache_store();
event_cache_store.test_save_event_updates_event_in_room_and_thread().await;
}
#[async_test]
async fn test_thread_vs_room_linked_chunk() {
let event_cache_store =
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::{Arc, RwLock as StdRwLock},
};
@@ -97,7 +97,7 @@ impl EventCacheStore for MemoryStore {
inner
.events
.apply_updates(linked_chunk_id, updates)
.map_err(|e| Self::Error::Backend(Box::new(e)))?;
.map_err(|e| Self::Error::Backend(Arc::new(e)))?;
Ok(())
}
@@ -188,10 +188,9 @@ impl EventCacheStore for MemoryStore {
) -> Result<Option<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let event = inner
.events
.items(room_id)
.find_map(|(event, _pos)| (event.event_id()? == event_id).then_some(event.clone()));
let event = inner.events.items(room_id).find_map(|(_, (event, _pos))| {
(event.event_id()? == event_id).then_some(event.clone())
});
Ok(event)
}
@@ -204,10 +203,10 @@ impl EventCacheStore for MemoryStore {
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
let inner = self.inner.read().unwrap();
let related_events = inner
let related_events: Vec<_> = inner
.events
.items(room_id)
.filter_map(|(event, pos)| {
.filter_map(|(linked_chunk_id, (event, pos))| {
// Must have a relation.
let (related_to, rel_type) = extract_event_relation(event.raw())?;
let rel_type = RelationType::from(rel_type.as_str());
@@ -219,14 +218,35 @@ impl EventCacheStore for MemoryStore {
// Must not be filtered out.
if let Some(filters) = &filters {
filters.contains(&rel_type).then_some((event.clone(), pos))
filters.contains(&rel_type).then_some((linked_chunk_id, (event.clone(), pos)))
} else {
Some((event.clone(), pos))
Some((linked_chunk_id, (event.clone(), pos)))
}
})
.collect();
Ok(related_events)
// Remove any duplicate events which may exist in both a room and thread
// linked chunk. Additionally, remove any position information from non-room
// linked chunks.
let mut deduplicated = HashMap::new();
for (linked_chunk_id, (event, position)) in related_events {
let event_id = event
.event_id()
.ok_or(Self::Error::InvalidData { details: String::from("missing event id") })?;
match linked_chunk_id.as_ref() {
LinkedChunkId::Room(_) => {
// Prioritize events that come from a room linked chunk
deduplicated.insert(event_id, (event, position));
}
_ => {
// Remove position information from events that come
// from any other type of linked chunk
deduplicated.entry(event_id).or_insert_with(|| (event, None));
}
}
}
Ok(deduplicated.into_values().collect())
}
async fn get_room_events(
@@ -237,17 +257,29 @@ impl EventCacheStore for MemoryStore {
) -> Result<Vec<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let event: Vec<_> = inner
let (_, event): (_, Vec<_>) = inner
.events
.items(room_id)
.map(|(event, _pos)| event.clone())
.map(|(_, (event, _pos))| event.clone())
.filter(|e| {
event_type
.is_none_or(|event_type| Some(event_type) == e.kind.event_type().as_deref())
})
.filter(|e| session_id.is_none_or(|s| Some(s) == e.kind.session_id()))
.collect();
.map(|e| {
e.event_id()
.map(|id| (id, e))
.ok_or(Self::Error::InvalidData { details: String::from("missing event id") })
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.fold((HashSet::new(), Vec::new()), |(mut ids, mut es), (id, e)| {
if !ids.contains(&id) {
ids.insert(id);
es.push(e);
}
(ids, es)
});
Ok(event)
}
@@ -28,8 +28,8 @@ mod memory_store;
mod traits;
use matrix_sdk_common::cross_process_lock::{
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
MappedCrossProcessLockState, TryLock,
CrossProcessLock, CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
CrossProcessLockGuard, MappedCrossProcessLockState, TryLock,
};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw};
@@ -64,32 +64,27 @@ impl fmt::Debug for EventCacheStoreLock {
impl EventCacheStoreLock {
/// Create a new lock around the [`EventCacheStore`].
///
/// The `holder` argument represents the holder inside the
/// [`CrossProcessLock::new`].
pub fn new<S>(store: S, holder: String) -> Self
/// The `cross_process_lock_config` argument controls whether we need to
/// hold the cross process lock or not.
pub fn new<S>(store: S, cross_process_lock_config: CrossProcessLockConfig) -> Self
where
S: IntoEventCacheStore,
{
let store = store.into_event_cache_store();
Self {
cross_process_lock: Arc::new(CrossProcessLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
holder,
)),
store,
}
let cross_process_lock = Arc::new(CrossProcessLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
cross_process_lock_config,
));
Self { cross_process_lock, store }
}
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
pub async fn lock(&self) -> Result<EventCacheStoreLockState, CrossProcessLockError> {
let lock_state =
self.cross_process_lock.spin_lock(None).await??.map(|cross_process_lock_guard| {
EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.clone() }
});
Ok(lock_state)
Ok(self.cross_process_lock.spin_lock(None).await??.map(|cross_process_lock_guard| {
EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.clone() }
}))
}
}
@@ -142,11 +137,11 @@ impl Deref for EventCacheStoreLockGuard {
}
/// Event cache store specific error type.
#[derive(Debug, thiserror::Error)]
#[derive(Clone, Debug, thiserror::Error)]
pub enum EventCacheStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
Backend(Arc<dyn std::error::Error + Send + Sync>),
/// The store is locked with a passphrase and an incorrect passphrase
/// was given.
@@ -159,7 +154,7 @@ pub enum EventCacheStoreError {
/// The store failed to encrypt or decrypt some data.
#[error("Error encrypting or decrypting data from the event cache store: {0}")]
Encryption(#[from] StoreEncryptionError),
Encryption(#[from] Arc<StoreEncryptionError>),
/// The store failed to encode or decode some data.
#[error("Error encoding or decoding data from the event cache store: {0}")]
@@ -167,7 +162,7 @@ pub enum EventCacheStoreError {
/// The store failed to serialize or deserialize some data.
#[error("Error serializing or deserializing data from the event cache store: {0}")]
Serialization(#[from] serde_json::Error),
Serialization(#[from] Arc<serde_json::Error>),
/// The database format has changed in a backwards incompatible way.
#[error(
@@ -193,13 +188,13 @@ impl EventCacheStoreError {
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
Self::Backend(Arc::new(error))
}
}
impl From<EventCacheStoreError> for CrossProcessLockError {
fn from(value: EventCacheStoreError) -> Self {
Self::TryLock(Box::new(value))
Self::TryLock(Arc::new(value))
}
}
+6 -7
View File
@@ -33,10 +33,10 @@ pub mod event_cache;
pub mod latest_event;
pub mod media;
pub mod notification_settings;
pub mod read_receipts;
mod response_processors;
mod room;
pub mod read_receipts;
pub mod sliding_sync;
pub mod store;
@@ -56,19 +56,18 @@ pub use client::{BaseClient, ThreadingSupport};
pub use http;
#[cfg(feature = "e2e-encryption")]
pub use matrix_sdk_crypto as crypto;
pub use once_cell;
pub use room::{
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp,
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
CallIntentConsensus, EncryptionState, PredecessorRoom, Room, RoomCreateWithCreatorEventContent,
RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp, RoomState, RoomStateFilter,
SuccessorRoom, apply_redaction,
};
pub use store::{
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
ThreadSubscriptionCatchupToken,
};
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawSyncStateEventWithKeys};
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawStateEventWithKeys};
#[cfg(test)]
matrix_sdk_test_utils::init_tracing_for_tests!();
+1 -1
View File
@@ -261,7 +261,7 @@ mod tests {
},
"iv": "AK1wyzigZtQAAAABAAAAKK",
"hashes": {
"sha256": "foobar",
"sha256": "/NogKqW5bz/m8xHgFiH5haFGjCNVmUIPLzfvOhHdrxY",
},
"v": "v2",
}))
+16 -17
View File
@@ -32,8 +32,8 @@ use std::fmt;
use std::{ops::Deref, sync::Arc};
use matrix_sdk_common::cross_process_lock::{
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
CrossProcessLockState, TryLock,
CrossProcessLock, CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
CrossProcessLockGuard, CrossProcessLockState, TryLock,
};
use matrix_sdk_store_encryption::Error as StoreEncryptionError;
pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
@@ -84,7 +84,7 @@ impl MediaStoreError {
impl From<MediaStoreError> for CrossProcessLockError {
fn from(value: MediaStoreError) -> Self {
Self::TryLock(Box::new(value))
Self::TryLock(Arc::new(value))
}
}
@@ -113,22 +113,20 @@ impl fmt::Debug for MediaStoreLock {
impl MediaStoreLock {
/// Create a new lock around the [`MediaStore`].
///
/// The `holder` argument represents the holder inside the
/// [`CrossProcessLock::new`].
pub fn new<S>(store: S, holder: String) -> Self
/// The `cross_process_lock_config` argument controls whether we need to
/// hold the cross process lock or not.
pub fn new<S>(store: S, cross_process_lock_config: CrossProcessLockConfig) -> Self
where
S: IntoMediaStore,
{
let store = store.into_media_store();
Self {
cross_process_lock: Arc::new(CrossProcessLock::new(
LockableMediaStore(store.clone()),
"default".to_owned(),
holder,
)),
store,
}
let cross_process_lock = Arc::new(CrossProcessLock::new(
LockableMediaStore(store.clone()),
"default".to_owned(),
cross_process_lock_config,
));
Self { cross_process_lock, store }
}
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
@@ -138,9 +136,10 @@ impl MediaStoreLock {
CrossProcessLockState::Clean(guard) => guard,
// The lock is dirty: another holder acquired it since the last time we acquired it.
// It's not a problem in the case of the `MediaStore` because this API is “stateless” at
// the time of writing (2025-11-11). There is nothing that can be out-of-sync: all the
// state is in the database, nothing in memory.
// It's not a problem in the case of the `MediaStore` because this API is
// “stateless” at the time of writing (2025-11-11). There is nothing
// that can be out-of-sync: all the state is in the database,
// nothing in memory.
CrossProcessLockState::Dirty(guard) => {
guard.clear_dirty();

Some files were not shown because too many files have changed in this diff Show More