Compare commits

...

885 Commits

Author SHA1 Message Date
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
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
Doug f1e41222f8 fix: Fix a deserialisation failure when the avatar_url is null. 2026-02-10 11:32:52 +01:00
Ivan Enderlin df6fc21576 refactor(sdk): Remove assume_has_waited_for_initial_prev_token.
This patch removes the `assume_has_waited_for_initial_prev_token` by
making `waited_for_initial_prev_token` to return a `&mut bool` instead
of `bool`.
2026-02-10 11:25:41 +01:00
Ivan Enderlin 72c1a655f2 refactor(sdk): waited_for_initial_prev_token is no more an Arc<AtomicBool>.
This patch changes `RoomEventCacheState::waited_for_initial_prev_token`
from `Arc<AtomicBool>` to `bool`. First off, the `Arc` wasn't used in
any useful way (never cloned for example). Second, the `AtomicBool` was
always used as a regular bool, no atomicity was really used. Lastly,
this patch adds the `assume_has_waited_for_initial_prev_token` method to
replace the `= true` operation to make code a bit more readable.
2026-02-10 11:25:41 +01:00
dependabot[bot] ae36751c9d chore(deps): bump CodSpeedHQ/action from 4.10.4 to 4.10.6
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.10.4 to 4.10.6.
- [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/fa0c9b1770f933c1bc025c83a9b42946b102f4e6...4deb3275dd364fb96fb074c953133d29ec96f80f)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-10 11:15:25 +01:00
Ivan Enderlin b1c4e45657 refactor(sdk): RoomEventCache::subscribe_to_pinned_events no longer takes a write lock.
This patch removes the write lock acquisition in
`RoomEventCache::subscribe_to_pinned_events` to replace it by read lock
acquisition. Nothing requires a `&mut self` at any point in this flow,
since `OnceLock::get_or_init` needs a `&self`.

The `RoomEventCacheStateLockWriteGuard::subscribe_to_pinned_events`
method is moved onto `RoomEventCacheStateLockReadGuard`.
2026-02-10 10:14:49 +01:00
Ivan Enderlin f0ee76c27f refactor(sdk): Remove one Vec allocation.
`LinkedChunk::push_items_back` expects an `IntoIter<Item = Event>`.
`EventLinkedChunk::push_live_events` has an `events` of kind `&[Event]`.
Using `events.iter().cloned()` instead of `events.to_vec()` not only
removes one `Vec` allocation, but it allows the compiler to apply more
optimisation.

Checking on godbolt.org, I see twice fewer LLVM IR lines, and 2.8x times
less ASM code.
2026-02-10 09:34:08 +01:00
Ivan Enderlin c1c48c2309 doc(sdk): Fix a typo for subscribe_to_pinned_events. 2026-02-10 08:36:31 +01:00
Ivan Enderlin b2fb7ad4e9 doc(sdk): Add #6143 to the CHANGELOG.md file. 2026-02-09 16:10:43 +01:00
Ivan Enderlin 6e20fb60fb fix(sdk): Restrict when m.room.member represents a LatestEvent candidate.
This patch relies on `MembershipChange` to decide when a `m.room.member`
represents a `LatestEvent` candidate. It was a mistale to rely on the
`membership` strictly, because the `prev_content` must be taken into
account.

Thus, this patch adds the following cases to `Knocked`, `Joined` and
`Invited`: `InvitationAccepted` and `KnockedAccepted`. Moreover, this
patch excludes other cases, including `ProfileChanged`, which was a bug
previously! When the user had a new display name, it was considered as a
`LatestEvent` candidate.
2026-02-09 16:10:43 +01:00
Benjamin Bouvier bc3ea6854b doc(event cache): explicit why we're stripping bundled relations from events before storing them in the event cache 2026-02-09 15:50:59 +01:00
Benjamin Bouvier 0ca9a0e6d1 refactor(event cache): extract send_updates_to_store as a common helper function 2026-02-09 15:50:59 +01:00
Benjamin Bouvier cc852c661f refactor(event cache): introduce persistence utils for the event cache
A bit of code has been duplicated for implementing the pinned events
linked chunk. This new module starts to common out a bit of it.
2026-02-09 15:50:59 +01:00
Ivan Enderlin 37c85fc774 chore(sdk): Downgrade a log from info to trace.
This patch changes an `info!` log to a `trace!`. Latest Events are
pretty stable now, and we don't get an info for each new computation
except when we want a proper trace.
2026-02-09 13:53:37 +01:00
Ivan Enderlin 6ef549e29a chore(sdk): Downgrade a log from error to info.
This patch changes an `error!` log to a `info!`. Indeed, this is not an
error to compute a Latest Event that doesn't exist yet. The system is
lazy purposely.
2026-02-09 13:53:37 +01:00
Doug b288ecbd25 Spaces: Check the power levels before removing an m.space.parent event. (#6132)
Small bug fix that makes `remove_child_from_parent` behave like
`add_child_to_parent`.

Additionally introduces a new Error case so that clients can decide to
ignore any failures updating the child → parent relationship.
2026-02-09 13:48:11 +02:00
Damir Jelić de3c5550a5 refactor(qr-login): Use the proper casing for an enum variant 2026-02-09 11:13:58 +01:00
Damir Jelić 8f04a48d1e refactor(qr-login): Remove the rendezvous_url method
MSC4388 doesn't use the full rendezvous URL in the QR code, instead we
just get the rendezvous ID.
2026-02-09 11:13:58 +01:00
Lakshya Nayak f787511616 fix(sliding-sync): re-dispatch invited/knocked rooms after reinvite (#6126)
## Problem

When a room goes through:

join -> leave/kick -> re-invite
Sliding Sync responses still include the room, but the SDK does not emit
an
Invited/Knocked update. Because of this, RoomListService is never
notified and
the room disappears from the room list until the application restarts.

## Root cause
"update_any_room()" only returned a RoomUpdateKind when one was
explicitly
produced. For re-invites it could return "None", causing the room to be
skipped.

## Fix
Always emit a default:
- "RoomUpdateKind::Invited"
- "RoomUpdateKind::Knocked"
when the room state is Invited/Knocked but no update kind was generated.

## Tests
Added a regression test covering:
join -> leave -> re-invite to ensure the room is surfaced again in the
invited list.

---------

Signed-off-by: Lakshya Nayak <89520692+VEL0C1TY22@users.noreply.github.com>
2026-02-09 10:44:59 +02:00
Damir Jelić ea58bc8139 Update ruma 2026-02-06 15:36:17 +01:00
dependabot[bot] 872bb095b6 chore(deps): bump time from 0.3.44 to 0.3.47
Bumps [time](https://github.com/time-rs/time) from 0.3.44 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.44...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 10:12:37 +00:00
Ivan Enderlin bff5c2f0e1 doc(base): Add #6130 to CHANGELOG.md. 2026-02-06 11:09:28 +01:00
Ivan Enderlin 627fe82ef3 test(base): Ensure that each state store has its own channel for RoomInfoNotableUpdates. 2026-02-06 11:09:28 +01:00
Ivan Enderlin 70faf2c3f3 fix(base): Move BaseClient::room_info_notable_update_sender into BaseStateStore.
This patch fixes a design issue. The
`BaseClient::room_info_notable_update_sender` is moved inside
`BaseStateStore` so that, when creating a new `BaseStateStore`, the
updates are not shared with other state stores. Updates are isolated to
the state store.

This bug has surfaced in `BaseClient::clone_with_in_memory_state_store`,
where a new `BaseStateStore` is created, but the
`room_info_notable_update_sender` was _cloned_, and that is a bug! We
could have re-created a new channel from scratch, but it would have
been hacky. Semantically, this channel should be part of the state store
itself. One proof is how it simplifies many call-sites, functions,
methods and structs: the `room_info_notable_update_sender` was passed
to multiple methods on `BaseStateStore`.
2026-02-06 11:09:28 +01:00
Jonas Richard Richter 0ec3db59f8 chore: move change to top of changelog 2026-02-06 09:38:50 +01:00
Jonas Richard Richter caa75981bb chore: add PR link to CHANGELOG.md 2026-02-06 09:38:50 +01:00
Jonas Richard Richter c49df20cd3 feat(ffi): include raw JSON of the underlying event in NotificationItem 2026-02-06 09:38:50 +01:00
Damir Jelić 036c5d35cd feat(crypto): Add a constructor for the MSC4388 variant of the QRCodeData struct 2026-02-05 21:17:44 +01:00
Benjamin Bouvier 80920e9ff2 refactor(common): use ruma's RelationType in the extractor functions 2026-02-05 16:43:25 +01:00
Benjamin Bouvier 071f982eed fix(event cache): don't include thread responses in the pinned event cache
There were two issues:

- first, `load_or_fetch_event_with_relations()` allowed to pass a
filter, but the filter wasn't taken into account when fetching relations
from the network. This would cause the initial load of pinned events to
also include thread responses, which we don't want.
- similarly, when adding related events from sync, we'd only look if an
event had a `m.relates_to` field; but it could be a thread response
being added in live.

The two issues are fixed similarly, by using a new `extract_relation`
serde helper that gives both the related_to event and the relation type.
That way, we can apply a manual filter in
`load_or_fetch_event_with_relations` after fetching relations from
network, and we can filter out live events based on the relation type.
2026-02-05 16:43:25 +01:00
Benjamin Bouvier 4aff3b566f refactor(event cache): use the background job monitoring for the pinned events task 2026-02-05 16:43:25 +01:00
Benjamin Bouvier eb8bd2d0b1 refactor(event cache): address review comments from 6085 2026-02-05 16:43:25 +01:00
Benjamin Bouvier 59fd7530f9 test(event cache): declare victory \o/ 2026-02-05 16:43:25 +01:00
Benjamin Bouvier 5778685352 feat(event cache): reload the pinned event cache at startup \o/ 2026-02-05 16:43:25 +01:00
Benjamin Bouvier d3742d2b30 feat(event cache): add support for redecryption in the pinned event cache 2026-02-05 16:43:25 +01:00
Benjamin Bouvier fcf0b87489 chore: address typos and cosmetic changes in an integration test 2026-02-05 16:43:25 +01:00
Benjamin Bouvier d7ecbc3c83 refactor(event cache): use the Room::load_or_fetch_events_with_relations now that it's widely available 2026-02-05 16:43:25 +01:00
Benjamin Bouvier f2279cd737 refactor(timeline): move load_event_with_relations to the RoomDataProvider trait
And get rid of the `PinnedEventsRoom` trait, and the accompanying file.
2026-02-05 16:43:25 +01:00
Benjamin Bouvier da5ac9e3e6 refactor(event cache): using proper locking for the pinned event cache 2026-02-05 16:43:25 +01:00
Benjamin Bouvier ca027f7eb6 feat(event cache): add a global EventCacheConfig struct to globally configure the event cache 2026-02-05 16:43:25 +01:00
Benjamin Bouvier 17840e52f5 refactor(timeline): start removing unused stuff in the timeline code
Now that this logic has moved to the event cache, it's not required in
the timeline anymore.
2026-02-05 16:43:25 +01:00
Benjamin Bouvier 2904735786 feat(timeline): subscribe to the pinned event cache from the timeline 2026-02-05 16:43:25 +01:00
Benjamin Bouvier 790a410474 feat(event cache): add a cache for a room's pinned events 2026-02-05 16:43:25 +01:00
Benjamin Bouvier 01bd672d05 feat(linked chunk): add a new ID for the pinned events linked chunk 2026-02-05 16:43:25 +01:00
Kévin Commaille 9ad7cf9662 refactor(base): Use PossiblyRedactedStateEventContent bound in MinimalStateEvent
We usually don't care if the event was redacted or not, we usually want
to no whether a field is set or not, so we don't need `Original` and
`Redacted` variants.

This simplifies several parts of the code since we don't have to handle
the intermediate enum to access the content now. Due to new APIs in
Ruma we can also just convert original and redacted event contents to
possibly redacted event contents.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-02-05 11:46:05 +01:00
Ivan Enderlin 8f156cbba8 feat(xtask): Display timeout in the duration graph of log sync.
This patch displays the timeout in the duration graph of `log sync`:

```
xxxxxxxxxuuuuu
```

where `x` represents the timeout, and `u` the duration.
2026-02-05 10:58:39 +01:00
Ivan Enderlin ae9fe44a7c feat(xtask): Add <time> in log sync reports.
This patch adds `<time>` around the values in the Time Column.
2026-02-05 10:58:39 +01:00
Ivan Enderlin edbddbdb55 feat(xtask): Display pos and timeout for sync report.
This patch updates the `cargo xtask log sync` command to extract the
`pos` and `timeout` fields so that we can display them. The `timeout`
is displayed in its own column, while the `pos` is displayed in the
line summary.
2026-02-05 10:58:39 +01:00
Ivan Enderlin 2ff33cd354 Merge pull request #6061 from mgoldenberg/consolidate-event-cache-store-tests
Consolidate integration tests for `EventCacheStore`
2026-02-05 09:20:04 +01:00
Ivan Enderlin 7675d22676 chore: Adjust uniffi features. 2026-02-05 09:02:29 +01:00
Ivan Enderlin af11c34399 chore: Adjust rand features. 2026-02-05 09:02:29 +01:00
Ivan Enderlin f51809d524 chore: Add default-features = false to more deps. 2026-02-05 09:02:29 +01:00
Ivan Enderlin 3ff9b55077 chore: Add more default-features = false to more deps. 2026-02-05 09:02:29 +01:00
Ivan Enderlin a35770b5d2 chore(crypto): Remove the indoc dep.
`indoc` is used once to declare a string, but it's easily doable with
pure Rust without too much boilerplate. Let's remove one macro dep.
2026-02-05 09:02:29 +01:00
Ivan Enderlin eb8b2c81f2 chore: Add more default-features to more deps. 2026-02-05 09:02:29 +01:00
Ivan Enderlin 034c1fcbcf chore: Disable default-features for decancer. 2026-02-05 09:02:29 +01:00
Ivan Enderlin f56f168b99 chore: Add more default-features = false to more deps. 2026-02-05 09:02:29 +01:00
Ivan Enderlin 386c5ee338 chore: Update camino to 1.2.2. 2026-02-05 09:02:29 +01:00
Ivan Enderlin 1b0b6a2f9a chore: Use the same version of quote, proc-macro2 and syn everywhere. 2026-02-05 09:02:29 +01:00
Ivan Enderlin d52d0d4a83 chore: xtask uses the workspace-defined clap.
Not only it uses the same version of `clap` everywhere, but it removes
7 dependencies.
2026-02-05 09:02:29 +01:00
Ivan Enderlin dc680b8594 chore: benchmarks uses the workspace-defined tokio. 2026-02-05 09:02:29 +01:00
Ivan Enderlin 44c2c01642 chore: Use default-features = false for all dependencies.
This patch adds `default-features = false` to all dependencies to avoid
fetching useless dependencies by default.

Numbers:

- `cargo check --workspace --tests` jumpbs from 725 to 715,
- `cargo tree --all-features --edges all --prefix none | rg -v '^$' |
  cut -d' ' -f 1 | sort | uniq | wc -l` jumps from 538 to 529.
2026-02-05 09:02:29 +01:00
Hugh Nimmo-Smith 7708087019 doc: clarify that IO_ELEMENT_MSC4388 is the unstable prefix 2026-02-04 20:45:00 +01:00
Ivan Enderlin 52014dc0bb chore(sdk): Log the pos and the timeout as part of the sync_once span.
This patch adds the `pos` and `timeout` value as new fields of the
`sync_once` span.

How to test it?

```sh
$ cargo nextest run --retries 0 --no-fail-fast -E "not test(ensure_no_max_concurrent)" -p matrix-sdk-ui --nocapture -- test_sync_all_states | rg sync_once
2026-02-04T14:02:17.043301Z DEBUG sync_once{conn_id="room-list" pos="0" timeout=0}:send{request_id="REQ-3" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="647B" status=200 response_size="72B" request_duration=316.959µs}: matrix_sdk::http_client: Got response
2026-02-04T14:02:17.043783Z DEBUG sync_once{conn_id="room-list" pos="1" timeout=0}:send{request_id="REQ-4" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="648B"}: matrix_sdk::http_client::native: Sending request num_attempt=1
2026-02-04T14:02:17.044093Z DEBUG sync_once{conn_id="room-list" pos="1" timeout=0}:send{request_id="REQ-4" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="648B" status=200 response_size="72B" request_duration=283.75µs}: matrix_sdk::http_client: Got response
2026-02-04T14:02:17.044527Z DEBUG sync_once{conn_id="room-list" pos="2" timeout=0}:send{request_id="REQ-5" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="648B"}: matrix_sdk::http_client::native: Sending request num_attempt=1
2026-02-04T14:02:17.044808Z DEBUG sync_once{conn_id="room-list" pos="2" timeout=0}:send{request_id="REQ-5" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="648B" status=200 response_size="72B" request_duration=254.875µs}: matrix_sdk::http_client: Got response
2026-02-04T14:02:17.045245Z DEBUG sync_once{conn_id="room-list" pos="3" timeout=30000}:send{request_id="REQ-6" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="648B"}: matrix_sdk::http_client::native: Sending request num_attempt=1
2026-02-04T14:02:17.045517Z DEBUG sync_once{conn_id="room-list" pos="3" timeout=30000}:send{request_id="REQ-6" method=POST uri="http://127.0.0.1:49663/_matrix/client/unstable/org.matrix.simplified_msc3575/sync" request_size="648B" status=200 response_size="72B" request_duration=247.417µs}: matrix_sdk::http_client: Got response
```
2026-02-04 16:24:37 +01:00
Benjamin Bouvier f3b9a01904 refactor(event cache): rename the read_lock_acquisition mutex 2026-02-04 16:24:23 +01:00
Benjamin Bouvier 2fb5a32db2 refactor(event cache): rename RoomEventCacheStateLockInner to RoomEventCacheState
There was nothing called `RoomEventCacheState` anymore, and the `Inner`
suffix is dubious, at best. Also, we can get rid of the `Lock`
component, since indeed it's locked, but it's a detail from the point of
view of the `RoomEventCacheState` itself. This makes for a shorter and
nicer name.
2026-02-04 16:24:23 +01:00
Benjamin Bouvier 9be5bc1977 feat(multiverse): render display names for messages and latest thread event 2026-02-04 14:27:10 +01:00
Jorge Martín 70dd02012b fix(ffi): Don't override the default package_name and cdylib_name values for Kotlin bindings
Otherwise, the bindings expect the generated JAR/AAR files to contain separate `.so` libraries for each crate
2026-02-04 10:53:29 +01:00
Jorge Martín f05e0b1b81 doc: Add missing changelog entry 2026-02-04 10:23:16 +01:00
Jorge Martín 7ffcf72483 fix(ffi): Remove UniFFI checksums in matrix-sdk crate
This was forgotten in a previous PR about removing the checksums for all crates exporting bindings.
2026-02-04 10:23:16 +01:00
Jorge Martín bb420360a4 Create reldev profile:
It allows having way smaller binaries while still being able to have proper backtraces: `reldbg` is great for iOS because it allows inline debugging using LLDB in Xcode, but it produces enormous binaries, while for Android we can't use that properly and we'd only be interested in having symbolicated backtraces, which this profile achieves with binaries an order of magnitude smaller.
2026-02-04 10:21:27 +01:00
Jorge Martín 9b6d54ef30 refactor: Enable debug-images feature for Sentry
This makes it possible to link a Sentry trace to the debug symbols so it can be symbolicated
2026-02-04 10:21:27 +01:00
Jorge Martín 9dc2901268 ci: Add dist profile
This is intended for reducing the binary size of the SDK distributed in Android/iOS bindings.

Its optimization level is 'binary size', it contains LTO optimizations and by default removes part of the debug info - the rest can be removed later if needed.
2026-02-04 10:21:27 +01:00
Benjamin Bouvier 50cc5f4102 refactor(pinned events): move pin_event/unpin_event from the Timeline to the Room
These make sense in general, and they will help getting rid of one of
the `PinnedEventsRoom` trait methods in a subsequent PR.
2026-02-04 09:59:47 +01:00
Benjamin Bouvier 1152eb6d37 refactor(room): move PinnedEventsRoom::load_event_with_relations to the Room object
The method is kept on the pinned loader trait at the moment, because
it's too inconvenient to remove it quite yet. This will happen in a
subsequent PR.
2026-02-04 09:39:18 +01:00
dependabot[bot] f270eda75d chore(deps): bump bytes from 1.11.0 to 1.11.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-04 10:23:35 +02:00
Jorge Martín 2b298fbb85 fix(ffi): Remove checksums in all crates using UniFFI
Previously, this was only applied to the FFI bindings crate, but it's not the only one affected by the issue with 32bit ARM checksum validations.
2026-02-04 09:06:06 +01:00
Michael Goldenberg f2abc555e2 doc(event-cache): update relevant change logs
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-03 12:14:47 -05:00
Michael Goldenberg d6293c75a9 test(event-cache): move test_linked_chunk_exists_before_referenced to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-03 11:47:05 -05:00
Michael Goldenberg d92b3ef781 feat(linked-chunk): use errors in RelationalLinkedChunk::apply_updates
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-03 11:08:46 -05:00
Michael Goldenberg ee1c95f6a3 refactor(linked-chunk): use errors in RelationalLinkedChunk::insert_chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-03 10:50:46 -05:00
Michael Goldenberg f68696c2a6 feat(linked-chunk): add error type for relational linked chunk
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-02-03 10:43:22 -05:00
Benjamin Bouvier 40c6c330b0 chore(typos): fix new typos 2026-02-02 18:36:01 +01:00
dependabot[bot] 7c86e1b896 chore(deps): bump crate-ci/typos from 1.42.1 to 1.43.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.42.1 to 1.43.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.42.1...v1.43.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 18:36:01 +01:00
dependabot[bot] 06700cdf38 chore(deps): bump CodSpeedHQ/action from 4.8.2 to 4.10.4
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.8.2 to 4.10.4.
- [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/e736f0d2aeb36da38e9f08eca4dff7967408d154...fa0c9b1770f933c1bc025c83a9b42946b102f4e6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 18:17:41 +01:00
Benjamin Bouvier 19234abf32 review(task_monitor): address comments, watchdog -> task_monitor, misc doc adjustments and renamings 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 24d3ee4433 doc(common): add a note around unwind safety for the task monitor 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 27965cf177 feat(multiverse): add a background job error observer in multiverse 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 50b06e8e5b feat(ffi): add support for listening to background jobs errors 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 3d5679954b refactor(event cache): use the task monitor for event cache long-running jobs 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 561b04cab0 feat(client): have the Client hold one task monitor 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 334bd04252 feat(common): allow aborting on drop for background tasks 2026-02-02 16:13:00 +01:00
Benjamin Bouvier 9195c000f3 feat(common): add a background task handler 2026-02-02 16:13:00 +01:00
Nashwan Azhari 28d1bd7ce3 doc: update Ruma doc links in get_profile example 2026-02-02 10:24:11 +01:00
Nashwan Azhari cb9690ecba doc: change matrix.org/docs/spec links to spec.matrix.org 2026-02-02 10:24:11 +01:00
Michael Goldenberg 78d681f24f style(event-cache): fmt
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-31 15:42:56 -05:00
Michael Goldenberg 709d4cb80d Merge branch 'main' into test-merge-consolidate-event-cache-store-tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-31 15:07:52 -05:00
Michael Goldenberg 3d32981dd4 doc(event-cache): document that MemoryStore is not transactional
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-30 23:46:26 -05:00
Michael Goldenberg d5de36badb test(event-cache): restore test_linked_chunk_update_is_a_transaction to indexeddb tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-30 23:31:27 -05:00
Michael Goldenberg ab3995fe26 test(event-cache): remove test_linked_chunk_update_is_a_transaction from integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-30 22:59:29 -05:00
Michael Goldenberg c641f5aea8 feat(event-cache): ensure chunks exist before referenced in indexeddb store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-30 22:50:24 -05:00
Michael Goldenberg 1b4e6aa0e7 feat(event-cache): ensure chunks exist before referenced in sqlite store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-30 22:36:22 -05:00
Michael Goldenberg 6e34585dca test(event-cache): remove extraneous integration tests
The tests removed are either covered by other tests or
ensure properties that shouldn't exist - e.g., that
new chunks can link to chunks that haven't been put
into the store yet.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-30 13:42:40 -05:00
Damir Jelić 2f7887d607 refactor(auth): Change the secure channel methods to operate more on strings
This will come in handy once we attempt to support MSC4388 as this MSC
changes how the client-server API operates. The rendezvous channel will
in the future operate on JSON messages where we'll include our sealed
ciphertext as a base64 encoded string.
2026-01-30 17:14:05 +01:00
Damir Jelić 837590d90e refactor(auth): Move the MSC4108-specific rendezvous channel implementation into a submodule 2026-01-30 17:14:05 +01:00
Damir Jelić 94bc870686 refactor(qr-login): Create a submodule for the rendezvous channel 2026-01-30 17:14:05 +01:00
Damir Jelić 911b559113 feat(crypto): Add support for the QR code data type defined in MSC4388 2026-01-30 16:37:17 +01:00
Ivan Enderlin 2ffe003136 doc(sdk) Add #6056 to the CHANGELOG.md. 2026-01-30 15:34:48 +01:00
Ivan Enderlin d348fe62f2 refactor(base): Use Iterator::find and Iterator::any. 2026-01-30 15:34:48 +01:00
Ivan Enderlin be2c1d44e0 feat(ffi) Add LatestEventValue::RemoteInvite. 2026-01-30 15:34:48 +01:00
Ivan Enderlin 8b26751a66 feat(ui): Add LatestEventValue::RemoteInvite.
This patch maps a `BaseLatestEventValue::RemoteInvite` to
`LatestEventValue::RemoteInvite`.
2026-01-30 15:34:48 +01:00
Ivan Enderlin 1dc1d2a3f0 feat(sdk): Add LatestEventValue::RemoteInvite.
This patch adds the `LatestEventValue::RemoteInvite` variant. The goal
of this is to be able to compute a `LatestEventValue` for an invite to
a room. Using `LatestEventValue::Remote` isn't possible because it's
usually built from the `RoomEventCache`. However, the `EventCache`
doesn't handle invites for one reason: invites only manipulate stripped
state-events, whist the `EventCache` manipulates non-stripped (state)
events.

The `LatestEvents` API receives a stream of `RoomInfoNotableUpdate`. It
reacts to update from the `RoomInfo`. It filters out all reasons except
`MEMBERSHIP`. When the `MEMBERSHIP` is updated, and the room' state is
`Invited`, then a `RemoteInvite` is computed.

The `Invite` type is updated to include the `inviter_id` in case the
`inviter` is missing. Indeed, we always know the user ID of the inviter,
this information isn't optional.
2026-01-30 15:34:48 +01:00
Ivan Enderlin 99b83b98b6 fix(base): Emit RoomInfoNotableUpdateReasons for invited and knocked rooms creation. 2026-01-30 15:34:48 +01:00
Ivan Enderlin 21448ab874 fix(sdk): Skip updates for missing rooms only.
This patch skips updates for missing rooms only, i.e. it doesn't early
return and then miss all other rooms.
2026-01-30 15:34:48 +01:00
Ivan Enderlin b203b43bb2 doc(sqlite): Add #6091 in the CHANGELOG.md. 2026-01-30 15:08:39 +01:00
Ivan Enderlin 09a1bf8ec2 fix(sqlite): Replace unwrap when using interact.
This patch replaces the `interact(…).unwrap()` by a proper error.

So far, `interact()` was only returning `InteractError::Panic`
despites `InteractError::Aborted` exists. With
https://github.com/deadpool-rs/deadpool/pull/461, we now get
`InteractError::Aborted` when the SDK is shutdown, sometimes. This
results in hitting the `unwrap` and having a panic again. This patch
solves the problem by changing the `unwrap` to a proper error. Note: in
case of `InteractError::Panic`, we continue to panic.

This patch makes sense with or without the merge of the PR on
`deadpool`.
2026-01-30 15:08:39 +01:00
Ivan Enderlin 7918a1817c doc(xtask): Add missing documentation for log and log sync. 2026-01-30 13:57:26 +01:00
Ivan Enderlin 43e1932691 feat(xtask): Introduce xtask log sync to visualise logs about sync.
This patch introduces a new family of commands: `xtask log`. The goal
is to manipulate logs, to extract the right amount of data we need to
solve specific problems. The first member of this family is `sync` to
visualise logs about the sync process. It presents the sync requests and
responses in a table, with a "timeline" _à la_ network profiler graph.
The code is rather simple, on purpose. The generated HTML reports are
lightweight, and fully standalone: no JavaScript, pure HTML and CSS, no
external resources. These reports can be shared or archived super
easily.

Features:

- requests/responses are grouped by connection ID
- permalink to specific request ID
- status have colours
- time is displayed in a human form
- duration is calculated from the log timestamps
- view syncs in a "tree-like" flavor, a "time graph", super quick to
  spot long requests
- each line can be "opened" to see details, so far only log line numbers
  to get more context manually
2026-01-30 13:57:26 +01:00
Damir Jelić 87ce49e14f refactor(crypto): Convert the QrCodeData type to a struct with named fields 2026-01-30 10:19:46 +01:00
Damir Jelić d4e8731edd Add a changelog entry about the QrCodeData struct updates 2026-01-30 10:19:46 +01:00
Damir Jelić b03c482c95 refactor(crypto): Remove the rendezvous_url method from the QrCode data type
MSC4388 won't have the full rendezvous URL encoded in the QR code,
instead it'll have a rendezvous ID.

So let's remove this accessors and use the `intent_data()` getter
instead.

MSC4388: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
2026-01-30 10:19:46 +01:00
Damir Jelić addafe2878 refactor(crypto): Create a MSC-agnostic QrCodeIntent enum
Since the intent is encoded differently in MSC4108 and MSC4388 it
doesn't make sense to publicly expose the binary constants for a
specific MSC in the public API.

This patch removes access to the binary constants and you only access to
a generic enum.
2026-01-30 10:19:46 +01:00
Damir Jelić c4a04eee97 refactor(crypto): Create a MSC and intent specific accessor for the QR login data type
This patch adds a view into the MSC-specific and intent specific data
fields of the QR login data type.

MSC4108 and MSC4388 have subtle differences in the way the rendezvous
URL and the server name are shared, this new getter allows us to access
all of those fields in a consistent manner.

MSC4108: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
MSC4388: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
2026-01-30 10:19:46 +01:00
Damir Jelić 97157c8fcf refactor(crypto): Use intent instead of mode in the field names of the qr code data type 2026-01-30 10:19:46 +01:00
Damir Jelić bfe42d9ccf refactor(crypto): Rename the QrLoginError into InvalidIntent 2026-01-30 10:19:46 +01:00
Damir Jelić 036fa5ca82 refactor(crypto): Rename QrCodeModeData into QrCodeIntentData 2026-01-30 10:19:46 +01:00
Damir Jelić 90651a3067 refactor(crypto): Rename QrCodeMode into QRCodeIntent 2026-01-30 10:19:46 +01:00
Damir Jelić c19ac306a1 refactor: Rename invalid version into invalid type 2026-01-30 10:19:46 +01:00
Damir Jelić 38731f12de refactor(crypto): Abstract away QrLoginData so we can support multiple MSC versions
This patch modifies the QrLoginData, it now hides all its public fields
and appropriate getters have been created for it instead.

This is necessary to hide the MSC specific parts of the data type thus
allowing support of multiple versions of the data type.
2026-01-30 10:19:46 +01:00
Damir Jelić 2d4053bed3 refactor(crypto): Create a MSC4108 submodule for the qr login types
Our current implementation of this QR code data type corresponds to the
data type defined in MSC4108. The data format has been updated a bit in
MSC4833 and thus we'll need to support both formats for a while.

This moves al the MSC4108-specific parts into a separate MSC-specific
submodule.

MSC4108: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
2026-01-30 10:19:46 +01:00
Damir Jelić faf7f5577a refactor(crypto): Create a submodule tree for the qr login types 2026-01-30 10:19:46 +01:00
Kévin Commaille 4feaa0ba49 Upgrade Ruma
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-30 10:16:06 +01:00
Damir Jelić 4e7b77d7d0 refactor(qr-login): Attempt to export the secrets bundle sooner in the login process
This ensures that, if we don't have a valid secrets bundle at hand we
error out as early es possible.
2026-01-30 10:15:26 +01:00
Stefan Ceriu 712c99f3cf chore(spaces): reduce m.space.child and m.space.parent deserialisation failure log levels as they are expected when removing either from the hierarchy 2026-01-29 19:42:16 +02:00
Andy Balaam 5d1382f507 Log our cross-signing and backup status after we receive a secret 2026-01-29 17:38:44 +00:00
Andy Balaam 8932869423 Log more information about gossip requests
So we can track which `m.secret.send` messages were successfully sent or
retried, and which secrets were contained in them.

Part of #6058
2026-01-29 13:24:15 +00:00
Tobias Fella 225644111c docs(sdk): Fix grammar 2026-01-29 14:18:18 +01:00
Kévin Commaille 1807a8765b refactor(crypto): Use to_canonical_value() directly
Instead of going through `serde_json::to_value()` and then converting it
to canonical JSON to serialize it.

Currently `to_canonical_value()` has the same behavior internally as
here, but an upcoming change in Ruma makes it use its own `Serializer`
so it is directly serialized as a `CanonicalJsonValue` which should be
somewhat more efficient.

This upcoming change also removes the `CanonicalJsonError::SerDe`
variant, so it is not as straightforward to propagate
`serde_json::Error`s.

This commit also removes outdated `#[allow(clippy::…)]` attributes.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-28 14:29:39 +01:00
Benjamin Bouvier c420d45e64 test(wasm): allow running the wasm tests with only one runner
Sometimes, running the wasm tests with a given runner will fail with
obscure, undecipherable reasons. As a result, it's convenient to be able
to locally run the wasm-pack tests with only a single runner, which this
commit allows.
2026-01-28 14:16:51 +01:00
Damir Jelić 1fb2ca5843 refactor(crypto-ffi): Serialize the secrets bundle when exporting it 2026-01-28 10:30:20 +01:00
Johannes Marbach 2ebab067b4 feat(timeline): enable focusing a thread root using TimelineFocus::Event
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-01-28 07:43:49 +01:00
Benjamin Bouvier e601c1d9b4 test: test that one can store the same event in multiple linked chunks 2026-01-27 16:02:20 +01:00
Benjamin Bouvier 2a968661e7 feat(sqlite): allow storing the same events in multiple linked chunks 2026-01-27 16:02:20 +01:00
Johannes Marbach 67a45b0772 feat(timeline): remove the obsolete event type filter
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-01-27 14:43:57 +01:00
Jonas Platte fccf44f285 fix: Add 'bellow' as a forbidden word (#6073)
Signed-off-by: Jonas Platte
2026-01-27 10:53:07 +00:00
Benjamin Bouvier 9f093737c4 doc(r2d2): tweak links and fix a few typos in the doc comment of r2d2 2026-01-27 11:27:12 +01:00
Jorge Martín 731cb0b426 fix(ffi): Temporarily remove checksums for Android bindings
There is an error in how JNA performs the API checksums that results in incorrect checks that make the app fail as soon as any SDK method is called, since initializing the SDK performs these checks
2026-01-27 11:21:11 +01:00
Benjamin Bouvier ae428446f9 test(http client): make the test_retry_limit_http_requests test more resilient
This test was setting a client-wide retry limit of 3 attempts for every
single network request. It happens that it was using the login method,
which unconditionally overrides this retry limit to 3 anyways, in
`LoginBuilder::send()`, so it worked only because the two retry limits
were accidentally in sync. Changing the retry limit in the test to 4
would make it thus fail; the test has been changed so it tries to use
the /whoami endpoint instead of login, as the former doesn't override
the retry limit.
2026-01-27 10:58:02 +01:00
Benjamin Bouvier d099239427 chore(http client): rename a variable to make it clearer what its role is
The `default_timeout` is a timeout value provided by `backon`, and
that's a suggestion of what the timeout value should be for the next
request (based on the backoff method used under the hood — in our case,
the exponential backoff). Since we have another concept of a default
timeout (the one present in the RequestConfig), it seems better to call
the timeout suggested by backon in a different manner, that's more
explicit in the given context.
2026-01-27 10:58:02 +01:00
dependabot[bot] b16d12568a chore(deps): bump oneshot from 0.1.11 to 0.1.13
Bumps [oneshot](https://github.com/faern/oneshot) from 0.1.11 to 0.1.13.
- [Changelog](https://github.com/faern/oneshot/blob/v0.1.13/CHANGELOG.md)
- [Commits](https://github.com/faern/oneshot/compare/v0.1.11...v0.1.13)

---
updated-dependencies:
- dependency-name: oneshot
  dependency-version: 0.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 10:02:51 +01:00
Jorge Martín db56d14177 fix(ui): Simplify event handler check in NotificationClient::try_sliding_sync
Past me: wtf?
2026-01-27 09:08:59 +01:00
Jorge Martín 4ee3a6d0a6 fix(ui): make m.room.avatar part of the required state for the sliding sync in the NotificationClient 2026-01-27 09:08:59 +01:00
Jorge Martín 19724cf2c2 fix(sdk): Fix latest event erasing RoomInfo
The sync lock was acquired too late in `LatestEvent::store`, which could lead to a race condition where we read some room info, that room info was modified and saved in parallel somewhere else, and then we modified the copy of the room info and overwrote that saved data with it, resulting in data loss.

In the clients, this was experienced as notifications sometimes lacking the room display name
2026-01-27 09:08:59 +01:00
dependabot[bot] fb0713275e chore(deps): bump CodSpeedHQ/action from 4.7.0 to 4.8.2
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.7.0 to 4.8.2.
- [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/0700edb451d0e9f2426f99bd6977027e550fb2a6...e736f0d2aeb36da38e9f08eca4dff7967408d154)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-27 08:52:17 +02:00
Johannes Marbach 8f4ac73ca9 feat(timeline): add filter for membership and profile changes
Use case: Display membership changes (join, leave, etc.) in the timeline but suppress profile changes (display name or avatar URL). This is currently not possible with `TimelineEventTypeFilter` because both types of changes have the same event type (`m.room.member`).

This pull request introduces a new `TimelineEventFilter` for filtering on either the event type or parts of its content. Content filters are only added for membership and profile changes but more enum variants can be added in future.
2026-01-26 14:50:49 +00:00
Skye Elliot 3924463c6d fix: Correctly store rooms with downloaded keys in SQLite and IndexedDB. (#6044)
While https://github.com/matrix-org/matrix-rust-sdk/pull/6017 is mostly
functional, there are two issues:

- I did not process `changes.room_key_bundles_fully_downloaded` in
`matrix-sdk-sqlite`, meaning any updates made via `Changes` would not be
persisted;
- I used a non-encrypting `JsValue` serialisation for the same field in
`matrix-sdk-indexeddb`, which causes errors when passed to the
decryption-enabled deserializer.

Solutions:

- Process the aforementioned changes such that keys are added to SQLite;
- Use a non-encrypting deserialiser, since this is effectively a
hash-set, and the contents aren't sensitive.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-01-26 14:12:54 +00:00
Johannes Marbach bd77f6673c chore(ffi): update uniffi to 0.31.0
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-01-26 14:05:04 +01:00
Kévin Commaille 3be8726afb test(ui): Test space child changes in SpaceService
Checks that adding and removing space children from sync works.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-26 10:23:45 +01:00
Michael Goldenberg e128f0fae3 test(event-cache): remove extraneous tests from indexeddb impl
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 19:12:39 -05:00
Michael Goldenberg 3c42e1b4b9 test(event-cache): move test_load_previous_chunk to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 18:57:43 -05:00
Michael Goldenberg 12af5c4444 test(event-cache): move test_load_last_chunk_with_cycle to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 18:48:11 -05:00
Michael Goldenberg 3738091689 test(event-cache): move test_load_last_chunk to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 18:44:46 -05:00
Michael Goldenberg 750f59e4ea test(event-cache): move test_filter_duplicate_events_no_events to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 18:41:52 -05:00
Michael Goldenberg 3486701602 test(event-cache): copy test_linked_chunk_update_is_a_transaction to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 18:41:41 -05:00
Michael Goldenberg e60b204cda test(event-cache): move test_linked_chunk_multiple_rooms to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 18:38:19 -05:00
Michael Goldenberg 0443615683 test(event-cache): copy test_linked_chunk_clear to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:54:18 -05:00
Michael Goldenberg d93238d086 test(event-cache): move test_linked_chunk_start_end_reattach_items to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:51:44 -05:00
Michael Goldenberg fe1ab18474 test(event-cache): move test_linked_chunk_detach_last_items to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:49:17 -05:00
Michael Goldenberg 2c8a915018 test(event-cache): copy test_linked_chunk_remove_item to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:46:19 -05:00
Michael Goldenberg 4f34b51bda test(event-cache): move test_linked_chunk_push_items to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:43:12 -05:00
Michael Goldenberg cd1fbef0ea test(event-cache): copy test_linked_chunk_remove_chunk to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:39:33 -05:00
Michael Goldenberg ca9c74d2c4 test(event-cache): move test_linked_chunk_new_gap_chunk to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:34:56 -05:00
Michael Goldenberg a7ab53838e test(event-cache): move test_linked_chunk_new_items_chunk to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:29:00 -05:00
Michael Goldenberg 680bc74543 test(event-cache): move test_linked_chunk_replace_item to integration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-25 17:26:00 -05:00
Kévin Commaille 3852129612 refactor(base): Improve logs
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-23 16:24:47 +01:00
Kévin Commaille be8abdc1cf test(sdk): Add tests about handling invalid state events
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-23 16:24:47 +01:00
Kévin Commaille e77e2ae18f fix(base): Handle sync state events that fail to deserialize
If the homeserver provides a state event to a client, it means that it
considers the event to be valid. If a state event is valid, it always
updates the state map of the room. So ignoring events that fail to
deserialize means that the local state map is different than the one
from the server.

In some cases the Matrix spec even explicitly says that if a required
field is missing from the content of a state event, it should be treated
as if the event is missing from the state map. And if a required field
is missing, the event will fail to deserialize.

So this handles state events very closely to how a server would we only
deserialize the event type and the state key first to make sure that a
valid state event always updates the local state map. Then we only
deserialize the events lazily when we encounter an event type that
updates the `RoomInfo`. Because we deserialize the event lazily and some
methods might edit parts of an event before passing it to `RoomInfo`,
the (possibly edited) deserialized event is cached alongside the raw
event and its keys to be able to pass it further down the chain.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-23 16:24:47 +01:00
Kévin Commaille 9590b3c683 refactor(base): Move state event decryption logic in separate function
To simplify the match arms of `dispatch`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-23 16:24:47 +01:00
Ivan Enderlin 9cf649eb60 fix(sdk): EventCacheInner::by_room can be a HashMap.
This patch replaces `HashMap` by `BTreeMap` in `EventCacheInner` as
we don't need any ordering. It is then faster to get (or insert) a new
`RoomEventCache`:

- an insert is O(1) for `HashMap` vs. O(log(n)) for `BTreeMap`,
- a get is O(1) for `HashMap` vs. O(log(n)) for `BTreeMap`.
2026-01-23 16:17:54 +01:00
Mauro Romito b0bc4b2af9 fix: better owner check comparison using the int type from ruma 2026-01-23 14:51:58 +01:00
Mauro Romito f524b28d36 fix: avoided creating an additional vector 2026-01-23 14:51:58 +01:00
Mauro Romito debc8e5e65 fix: adjusted an issue with the test not, including the creator in the PL which is not allowed on v12 2026-01-23 14:51:58 +01:00
Mauro Romito 50a5a1d0a8 refactor: refactored LeaveSpaceRoom to also account for v12 rooms where creators have infinite PL and users with PL 150 are considered owners. Included also are_creators_privileged to let clients know if we are in a v12 room.
# Conflicts:
#	crates/matrix-sdk-ui/src/spaces/leave.rs

# Conflicts:
#	crates/matrix-sdk-ui/src/spaces/leave.rs

# Conflicts:
#	bindings/matrix-sdk-ffi/src/spaces.rs
#	crates/matrix-sdk-ui/src/spaces/leave.rs
2026-01-23 14:51:58 +01:00
Damir Jelić 6d5a43b47a feat(crypto-ffi): Add bindings to export a secrets bundle 2026-01-23 14:46:16 +01:00
Damir Jelić 4fcb553880 feat: Add uniffi headers to some more types
This patch allows uniffi to re-export some types related to secret
bundles.
2026-01-23 14:46:16 +01:00
Ivan Enderlin a1b3cd48fe doc(common): Update the CHANGELOG.md. 2026-01-23 13:55:21 +01:00
Ivan Enderlin 77515dda8e fix(common): Fix an off-by-one index removal in LinkedChunk::remove_item_at.
This patch fix an off-by-one check for `Error::InvalidItemIndex` in
`LinkedChunk::remove_item_at`.

This patch updates the `test_remove_item_at` test to cover this bug.
2026-01-23 13:55:21 +01:00
Kévin Commaille e69f9e4f89 refactor(base): Remove unused beacons from BaseRoomInfo
This field was added already unused in the initial PR
https://github.com/matrix-org/matrix-rust-sdk/pull/3741 for live
location sharing.

The follow up live location PRs didn't make use of it either:

- https://github.com/matrix-org/matrix-rust-sdk/pull/3771
- https://github.com/matrix-org/matrix-rust-sdk/pull/3794
- https://github.com/matrix-org/matrix-rust-sdk/pull/4025

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-22 18:25:48 +02:00
Benjamin Bouvier 3989e483d5 chore(send queue): save the thumbnail as the thumbnail of itself 2026-01-22 13:31:49 +01:00
Benjamin Bouvier ab53ec7c67 test(timeline): add a test that a thumbnail can be loaded without network access, after we sent it 2026-01-22 13:31:49 +01:00
Benjamin Bouvier 3b339be422 refactor(media): remove the now unused make_local_thumbnail_media_request 2026-01-22 13:31:49 +01:00
Benjamin Bouvier 32666e4c24 feat(send queue): cache a thumbnail as such for the media that we just uploaded
Before: after uploading a media and a thumbnail with the send queue, the
thumbnail would be cached only as a "file", and not as a thumbnail. This
is wasteful, if the embedder is interested in getting a thumbnail of the
exact same dimensions, for the file they've updated; there's no good
reason to wait for the server to return it back.

However, there were good reasons to store it as a file in the past. So,
we're choosing here to duplicate the thumbnail in the media store:
- it's saved as a file for its own MXC URI (which preserves the previous
behavior)
- it's also saved as a thumbnail for the media MXC URI (which implements
the desired behavior).

Tests are updated to reflect this.
2026-01-22 13:31:49 +01:00
Benjamin Bouvier a32c11f5ee chore: make clippy happy 2026-01-20 17:07:46 +01:00
Benjamin Bouvier fe885360cc refactor: move the thread read receipt computation to its own function 2026-01-20 17:07:46 +01:00
Benjamin Bouvier 070d17a2d0 refactor: group common match arms together 2026-01-20 17:07:46 +01:00
Benjamin Bouvier 1c0bcf4a69 feat(timeline): update thread summaries when there's a new read receipt event 2026-01-20 17:07:46 +01:00
Benjamin Bouvier 8ae4acdc00 feat(timeline): expose the user's threaded receipt on each thread summary
This is half of the work: this will load the threaded receipt for each
thread, every time we add/update a timeline item for an event that had a
thread summary. Since we don't know which of the private or the public
receipt is the most advanced, we simply pass both, to start with; it's
expected that this code dies later, when we fold it in into the event
cache.

The second half will consist in updating the thread summaries when a new
read receipt event happens.
2026-01-20 17:07:46 +01:00
Doug 439fb9d9c9 chore: Remove the redundant LeaveSpaceRoom.joined_members_count property. 2026-01-20 15:45:59 +02:00
Ivan Enderlin b3aa849f87 feat(sdk): New state event candidate for LatestEventValue: m.room.member with join.
This patch adds a new state event candidate for `LatestEventValue`:
`m.room.member` when the `membership` is `join` and the `state_key`
is the current user ID. Put differently: when the current user joins a
room, we are able to compute a `LatestEventValue`.
2026-01-20 14:43:18 +01:00
Stefan Ceriu a9ccb443bc feat(room_list): Add a room identifier based list room list service filter 2026-01-20 15:05:59 +02:00
Stefan Ceriu 0cca375f03 feat(spaces): Expose a SpaceFilter API
The `SpaceFilter`s API provides a simple interface that can be used in conjuncture with the `RoomList` to filter down the hierarchy to a particular space or its descendants.

Per design, the first level `SpaceFilter`s will only contain direct descendants while the second level ones will contain the rest of the hierarchy recursively.

The full feature is defined in https://github.com/element-hq/element-meta/issues/2966
2026-01-20 15:05:59 +02:00
Stefan Ceriu c5bdeb26ed chore(spaces): Refactor SpaceRoom sorting and move the core logic outside of the space room list
Rooms will soon need to be sorted outside of /hierarchy responses and as such the sorting algorithm will need to be shared between multiple users.
2026-01-20 15:05:59 +02:00
Doug d278f20dd4 feat: Add a method to reset a SpaceRoomList. 2026-01-20 14:12:15 +02:00
Damir Jelić a3111e7541 Merge pull request #5931 from JoFrost/main
feat[bindings]: expose power level thresholds in corresponding timeline event
2026-01-20 11:50:00 +01:00
dependabot[bot] b96b7bc71f chore(deps): bump CodSpeedHQ/action from 4.5.2 to 4.7.0
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.5.2 to 4.7.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/dbda7111f8ac363564b0c51b992d4ce76bb89f2f...0700edb451d0e9f2426f99bd6977027e550fb2a6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 11:15:48 +01:00
dependabot[bot] 3ff49becbe chore(deps): bump crate-ci/typos from 1.42.0 to 1.42.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.42.0 to 1.42.1.
- [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.42.0...v1.42.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 08:45:25 +01:00
Mauro Romito 3150dd4628 change: code improvements suggested in the pr review 2026-01-20 08:41:49 +01:00
Mauro Romito a46b006473 refactor: added the check for the joined_members_count inside the test_leave 2026-01-20 08:41:49 +01:00
Mauro Romito cd5b8eefe0 doc: updated the changelog
# Conflicts:
#	bindings/matrix-sdk-ffi/CHANGELOG.md
2026-01-20 08:41:49 +01:00
Mauro Romito bd8041765f fix: updated the tests so that they also account for the membership state 2026-01-20 08:41:49 +01:00
Mauro Romito 9183c80ea6 fix: is_last_admin also accounts for the membership state and included the joined_members_count in LeaveSpaceRoom 2026-01-20 08:41:49 +01:00
dependabot[bot] b68b6b95b0 chore(deps): bump qmaru/wasm-pack-action from 0.5.2 to 0.5.3
Bumps [qmaru/wasm-pack-action](https://github.com/qmaru/wasm-pack-action) from 0.5.2 to 0.5.3.
- [Release notes](https://github.com/qmaru/wasm-pack-action/releases)
- [Commits](https://github.com/qmaru/wasm-pack-action/compare/v0.5.2...v0.5.3)

---
updated-dependencies:
- dependency-name: qmaru/wasm-pack-action
  dependency-version: 0.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-20 09:14:53 +02:00
ganfra c9ac788172 fix(spaces): Allow removing unknown child rooms from spaces 2026-01-19 16:59:25 +00:00
JoFrost eb13eeeacd chore[changelog]: move new changes to the top 2026-01-19 17:52:00 +02:00
JoFrost 2ff1f66f37 chore[ffi]: rename previous to previous_users 2026-01-19 17:51:38 +02:00
Viktor Strate Kløvedal 0053ecb5f4 FFI: implement Room::list_threads (#5953)
Expose `Room::list_threads` to the FFI.

---------

Signed-off-by: viktorstrate <viktorstrate@gmail.com>
2026-01-19 13:32:29 +00:00
Michael Goldenberg d1c8b1090b fix(sdk): ensure IdentityStatusChanges not dropped prematurely
For details, see https://github.com/matrix-org/matrix-rust-sdk/issues/4599

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-19 09:32:15 +01:00
Michael Goldenberg 13b3e0349e chore(deps): bump async-stream to 0.3.6
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2026-01-19 09:32:15 +01:00
Skye Elliot 90390d7488 Merge pull request #6017 from matrix-org/kaylendog/history-sharing/backup-download
feat: Download room keys from backup before building key bundle.
2026-01-16 17:03:46 +00:00
Benjamin Bouvier 5b8ff8a76d refactor(state store): remove StateStore::upsert_thread_subscription
There is `StateStore::upsert_thread_subscriptions` as a proper
replacement these days.
2026-01-16 17:01:03 +01:00
Ivan Enderlin e9f5ed108a test(sdk): Add test_latest_event_is_recomputed_when_a_user_is_ignored.
This patch adds a test to ensure the `LatestEventValue` is re-computed
(more specifically: erased) when a user is ignored.
2026-01-16 15:01:52 +01:00
Ivan Enderlin 34ef8fc324 fix(sdk): The LatestEventValue is erased when a room has been emptied.
This patch ensures that a `LatestEventValue` is erased when a room has
been emptied.

If we are computing a value from the Event Cache, it's because we
have received an update from the Event Cache. This update falls in two
categories: either an event has been added or updated, or the room has
been emptied. We consider the room has been emptied by default. If we
are able to scan at least one in-memory event, we consider the room has
not been emptied.

This patch adds one specific, and updates other tests that were using
an empty Event Cache (which now produces a different result in this
situation).
2026-01-16 15:01:52 +01:00
Ivan Enderlin 92a7d033a2 chore(sdk): Split latest_events::latest_event.
This patch splits the `latest_events/latest_event.rs`
module into `latest_events/latest_event/mod.rs` and
`latest_events/latest_event/builder.rs`. The file was too big and asked
for a diet. The `LatestEventValueBuilder` type has been renamed to
`Builder`, and the `LatestEventVAluesForLocalEvents` has been renamed
to `BufferOfValuesForLocalEvents` for the sake of clarity and shorter
names.
2026-01-16 14:37:46 +01:00
mlm-games cb8a9995ac fix(widget): Do not include uniffi attrs when the feature is not enabled for element_call.rs 2026-01-16 14:19:16 +01:00
Jorge Martín fb3c1f8ace doc: Add changelog 2026-01-16 12:48:15 +01:00
Jorge Martín 53b5f04b15 fix(ffi): Use the new RoomPowerLevelsContentOverride API when creating a room
This fixes an issue that prevented default values coming from the SDK from being uploaded in the create room request, which could mean rooms created with the wrong power levels if the default values in the homeserver didn't match those of the SDK
2026-01-16 12:48:15 +01:00
Jorge Martín 4f6b4bf709 feat: Bump Ruma to commit hash version to fix a room power level related bug 2026-01-16 12:48:15 +01:00
Skye Elliot 8a89726726 docs: Update CHANGELOGs. 2026-01-16 11:44:45 +00:00
Ivan Enderlin fbff1ee99a bench: Bump codspeed-criterion-compat to 4.2.1. 2026-01-15 12:50:25 +01:00
Ivan Enderlin 734cc5b77e fix: Patch ruma to fix #5979.
This patch includes https://github.com/ruma/ruma/pull/2329.
2026-01-15 12:50:25 +01:00
JoFrost 19c958add0 Merge branch 'main' into main
Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2026-01-14 20:18:18 +02:00
Doug d64c990658 feat: Add a method to set your own user's display name within a room
For e.g. the /myroomnick slash command.
2026-01-14 14:15:22 +02:00
Skye Elliot e156d8e00c refactor: Use Try operator over Into::into, fixup comment. 2026-01-13 17:46:54 +00:00
Skye Elliot 831ab6d429 tests: Check historic room keys fetched from backup.
- Splits `test_secret_gossip_after_interactive_verification ` into a helper method.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-01-13 15:47:02 +00:00
Skye Elliot 3baaef5109 feat: Download room keys from backup before building key bundle.
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-01-13 15:19:28 +00:00
Ivan Enderlin 11d430563b doc(sdk): Update the `CHANGELOG.md. 2026-01-13 13:26:16 +01:00
Ivan Enderlin bfeafa9948 feat(sdk): Introduce the PollTimeout type.
This patch introduces the `PollTimeout` type to represent
either no timeout with `PollTimeout::None`, some timeout
with `PollTimeout::Some(_)`, or a default timeout with
`PollTimeout::Default`. It's finer than the previous `bool` that
was used, where `false` meant `PollTimeout::None`, and `true` meant
`PollTimeout::Default`. It's now possible to pass a precise timeout
value.
2026-01-13 13:26:16 +01:00
Johannes Marbach dfd607f195 feat(timeline): utilize the cache and include common relations when focusing on an event without context
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2026-01-13 11:37:36 +01:00
JoFrost 9b15400936 feat[ffi]: Add nonfavorite room filter (#5991)
Hello!

As the `Not` filter is not available on the FFI SDK due to UniFFI
constraints, this PR adds a NonFavorite filter to RoomList, implemented
as the negation of the existing Favorite filter. This was made to
address the issue #5978.

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

<!-- Sign-off, if not part of the commits -->
<!-- See CONTRIBUTING.md if you don't know what this is -->
Signed-off-by:

---------

Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2026-01-13 11:35:54 +01:00
dependabot[bot] edc1aee471 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 6229aa757e3e8a028bd97a49e190207e108eefbd to 78beac95c8fd7c25bdfb194415128523e41512d5.
- [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/6229aa757e3e8a028bd97a49e190207e108eefbd...78beac95c8fd7c25bdfb194415128523e41512d5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-13 09:01:32 +01:00
Benjamin Bouvier 387df77a6f fix(latest event): mark a failed but recoverable attempt to send as a sending
In the send queue, failures to send can be classified into two
categories:

- permanent failures (e.g. invalid parameters)
- recoverable failures (e.g. network is down; server responded with a
  transient error code)

The latest event system would classify all the failures as "cannot be
sent", which is slightly incorrect if the failure was recoverable. In
this case, we should still consider the local event as being sent, as
the system should try to send it some time soon.
2026-01-12 16:36:10 +01:00
Benjamin Bouvier b9ef07a719 refactor: commonize two arms of a match 2026-01-12 16:36:10 +01:00
Benjamin Bouvier df49cbb44d refactor(test): deduplicate test helper related to local latest event 2026-01-12 16:36:10 +01:00
dependabot[bot] b1ca2bbbb5 chore(deps): bump crate-ci/typos from 1.40.0 to 1.41.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.40.0 to 1.41.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.40.0...v1.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 13:28:00 +00:00
Ivan Enderlin ccbb7575c0 doc(base): Move an entry at its correct place. 2026-01-12 14:07:35 +01:00
Richard van der Hoff 5be3ea3d46 common: Fix up documentation on TimelineEvent
https://github.com/matrix-org/matrix-rust-sdk/pull/4568 merged together
`SyncTimelineEvent` and `TimelineEvent`, but removed some of the useful
documentation on `TimelineEvent`, and left behind some confusing
references. This change fixes it up.
2026-01-12 10:03:04 +01:00
Ivan Enderlin 7b16a32910 doc: Update CHANGELOG.mds. 2026-01-09 15:46:48 +01:00
Ivan Enderlin 7caac55580 chore(ui): RoomListService:subscribe_to_rooms replaces old subscriptions.
This patch changes `subscribe_to_rooms` in `RoomListService` to replace
the old subscriptions. It avoids accumulating subscriptions forever and
is closer to the old `visible_rooms` sliding sync list behaviour.
2026-01-09 15:46:48 +01:00
Ivan Enderlin f19249c47d feat(sdk): Add SlidingSync::(clear_and_subscribe|unsubscribe)_to_rooms.
This patch adds two methods on `SlidingSync`: `unsubscribe_to_rooms`
and `clear_and_subscribe_to_rooms` to respectively remove many room
subscriptions, and to replace room subscriptions by new ones.
2026-01-09 15:46:48 +01:00
Valere 6c067a7981 add changelog 2026-01-09 13:49:23 +01:00
Valere f2991134f2 test: Call intent serialization tests 2026-01-09 13:49:23 +01:00
Valere e39a23e5ae feat(call): Add new call intents for voice only 2026-01-09 13:49:23 +01:00
Richard van der Hoff eba2a57122 common: fix typos in changelog
Some typos in the #5959 changelog
2026-01-09 13:47:40 +01:00
Jorge Martín 7595ae0916 test(ffi): Add test for CreateRoomParameters -> create_room::v3::Request transformation 2026-01-09 12:08:56 +01:00
Jorge Martín faff2d70f8 feat(ffi): Add ffi::CreateRoomParameters::is_space
This allows us to create a room with a `RoomType::Space` from the clients
2026-01-09 12:08:56 +01:00
Ivan Enderlin f3aec8d33e doc(sdk): Remove 6002 as it's no more relevant. 2026-01-09 11:25:37 +01:00
Ivan Enderlin 632ca3f9bf chore(sdk): Sliding Sync room_subscriptions is definitely not sticky.
Our information were wrong. `room_subscriptions` is definitely not
sticky, we must send them for each request.

This patch removes `RoomSubscriptionState`. Subscriptions are not marked
as “applied” anymore, they are always sent.
2026-01-09 11:25:37 +01:00
JoFrost 5291ff2b4d Merge branch 'main' into main
Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2026-01-07 19:58:20 +02:00
Richard van der Hoff 97110edbe1 Merge pull request #6007 from matrix-org/kaylendog/history-sharing/changelog-fix
docs(common): Correct `ForwarderInfo` changelog entry.
2026-01-07 16:57:59 +00:00
Skye Elliot fbc0981e8d docs(common): Correct ForwarderInfo changelog entry.
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-01-07 16:10:24 +00:00
Jorge Martín 6b0c1e2992 doc: Add doc and inline comments 2026-01-07 16:11:40 +01:00
Jorge Martín c65026b70a doc: Add changelog entry 2026-01-07 16:11:40 +01:00
Jorge Martín b5a0042e14 fix(sqlite): Add WAL checkpoints to the DBs when they're opened too 2026-01-07 16:11:40 +01:00
Jorge Martín 3be6fb1a80 fix(sqlite): Add WAL checkpoints when vacuuming
For some reason, the automatic WAL checkpoints don't seem to be working as expected. Since we should periodically run VACUUM operations, we might as well add checkpoints before vacuuming (so the WAL size is reset and can grow to fit the whole DB) and after (so we clean up after that).
2026-01-07 16:11:40 +01:00
Ivan Enderlin 5dcd877dcd test(ui): Sticky parameters have been removed. 2026-01-07 14:05:59 +01:00
Ivan Enderlin ed1c847e7b doc(sdk): Update the `CHANGELOG.md. 2026-01-07 14:05:59 +01:00
Ivan Enderlin 87962d10e2 test(sdk): Test Request::extensions::to_device::since is set.
This patch restores a couple of assertions from a recently removed
test where it is asserted that `Request::extensions::to_device::since`
is set from the Olm machine.
2026-01-07 14:05:59 +01:00
Ivan Enderlin 4ac4ad3440 chore(sdk): Remove the sticky_parameters module.
This patch removes the `sticky_parameters` module, which is now unused.
2026-01-07 14:05:59 +01:00
Ivan Enderlin 742b6e5200 chore(sdk): Sliding Sync list filters is no longer sticky.
This patch extracts `SlidingSyncListStickyParameters::filters` to
no longer make it sticky. We are dropping sticky parameters as it's not
part of the last MSC.
2026-01-07 14:05:59 +01:00
Ivan Enderlin 43657d8302 chore(sdk): Sliding Sync list required_state is no longer sticky.
THis patch extracts `SlidingSyncListStickyParameters::required_state` to
no longer make it sticky. We are dropping sticky parameters as it's not
part of the last MSC.
2026-01-07 14:05:59 +01:00
Ivan Enderlin b28999e40c chore(sdk): Inline and always apply SlidingSyncStickyManager.
This patch removes `SlidingSyncStickyManager` as it only contains
the logic for a single request field: `room_subscriptions`. Also,
previously, `room_subscriptions` was considered sent based on the
transaction ID. This is useless as it's always sticky per MSC4186. The
logic from `SlidingSyncStickyManager` is sent inlined, allowing to
effectively remove this type.
2026-01-07 14:05:59 +01:00
Ivan Enderlin 42de8307bf chore(sdk): Sliding Sync extensions is no longer sticky.
This patch extracts `SlidingSyncStickyParameters::extensions` to no
longer make it sticky. We are dropping sticky parameters as it's not
part of the last MSC.
2026-01-07 14:05:59 +01:00
Jorge Martín 48d1d1f80f doc: Add changelog entry 2026-01-07 11:25:31 +01:00
Jorge Martín d52e5cfb50 doc: Add more doc comments to Client::fetch_client_well_known 2026-01-07 11:25:31 +01:00
Jorge Martín fe46a0cce0 fix(sdk): When using fetch_client_well_known_with_url, use the server name from the Client::user_id as a possible fallback too 2026-01-07 11:25:31 +01:00
ragebreaker b081654c51 fix(search): Create key dirs if they don't exist. (#5992)
The issue is related to the encrypt_store_dir fn where, when creating
the key file, it doesn't ensure that the parent directory exists first.
It might be not optimal for the user of the crate to ensure in an non
hacky manner, as the sdk iterates through most of its directories
internally. Have a test to verify it, which can be removed later (if
being merged)

Needs a review, might not be the optimal solution as this is my first pr
with the crate and am not that familiar with it (although do use it in
one of my apps).
2026-01-07 11:07:36 +01:00
Skye Elliot cc622a56ea Merge pull request #6000 from matrix-org/kaylendog/history-sharing/ui
Expose information about room key bundle forwarder in `matrix-sdk-ui` and `matrix-sdk-ffi`.
2026-01-06 16:40:10 +00:00
Skye Elliot a5b1231f8c refactor: Deduplicate shared history test code to helper methods. 2026-01-06 16:18:33 +00:00
Skye Elliot a8c9257dfe docs: Update CHANGELOGs. 2026-01-06 16:16:20 +00:00
Skye Elliot bfdd3ccc07 test: Ensure forwarder info is not available on unshared events. 2026-01-06 15:00:33 +00:00
Skye Elliot 503234976f refactor: Extract forwarder data fetcing to a helper function. 2026-01-06 14:28:44 +00:00
Skye Elliot 6fdd83478a tests: Ensure forwarder info accessible via high-level API. 2026-01-06 12:59:54 +00:00
Skye Elliot 0a5a22ec6f feat(ui): Expose information about room key bundle forwarder.
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-01-06 12:15:27 +00:00
dependabot[bot] 21cad56213 chore(deps): bump CodSpeedHQ/action from 4.4.1 to 4.5.2
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.4.1 to 4.5.2.
- [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/346a2d8a8d9d38909abd0bc3d23f773110f076ad...dbda7111f8ac363564b0c51b992d4ce76bb89f2f)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-06 12:02:50 +01:00
Kévin Commaille b0a536aaeb Upgrade Ruma
Uses the newly released version.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-06 12:02:23 +01:00
Kévin Commaille 5ee379d588 fix(ui): Deduplicate aggregation local and remote echo
We can have 3 different states for the same aggregation in
related_events, in chronological order:

1. The local echo with a transaction ID.
2. The local echo with the event ID returned by the server after
   sending the event.
3. The remote echo received via sync.

The transition from states 1 to 2 was already handled in
`mark_aggregation_as_sent()`.
But the transition from states 2 to 3 was never handled and we ended up
with both the local echo and the remote echo in the related events.

This resulted in the local echo being chosen over the remote echo when
computing the latest edit only because it was first in the list, even
though it didn't contain the raw JSON of the edit.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2026-01-06 10:00:24 +00:00
Jonas Platte ebbf34e924 Fix more new clippy lints 2026-01-06 09:51:10 +00:00
Jonas Platte e4aff871de Refactor IndexeddbStateStore::save_changes 2026-01-06 09:51:10 +00:00
Jonas Platte 0a9994c529 Fix new clippy lints 2026-01-06 09:51:10 +00:00
Jonas Platte 06764e2542 Reformat matrix-sdk-indexeddb 2026-01-06 09:51:10 +00:00
Jonas Platte 84cd2a67d4 Upgrade matrix-sdk-indexeddb to Rust edition 2024 2026-01-06 09:51:10 +00:00
Richard van der Hoff e3867aa7df Merge pull request #5959 from matrix-org/rav/clean_up_shield_state
Remove unused `ShieldStateCode::SentInClear`

`VerificationState::to_shield_state_{strict,lax}` return a type `ShieldState`, whose inner
type `ShieldStateCode` currently includes a variant `SentInClear` for "unencrypted event".
This is very misleading, because those functions never actually set that variant.

Rather, there is a separate method in `matrix-sdk-ui`, `EventTimelineItem::get_shield`,
which uses the same type, but *does* set that variant where appropriate.

As a user of matrix-sdk-common, without the matrix-sdk-ui layer, this is dangerously
misleading: it gives the impression that we are checking for unencrypted events, when in
fact we are not.

The solution seems to be to use different types for the different levels of the stack.

While we're at it, we fix up some of the confusion of methods that return an `Option` of an
enum type which itself has a `None` variant.
2026-01-05 16:33:54 +00:00
JoFrost 4c59282822 chore[ffi]: clippy 2025-12-21 22:22:19 +02:00
JoFrost a68aeafb4b chore[ffi]: rebase on last changes introduced in the events pr 2025-12-21 22:18:55 +02:00
JoFrost bc33878c64 Merge branch 'main' into main
Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2025-12-21 22:13:43 +02:00
Skye Elliot cd9f433358 Merge pull request #5945 from matrix-org/kaylendog/history-sharing/encryption-info
feat: Add `forwarder: ForwarderInfo` to `EncryptionInfo`.

Introduces `ForwarderInfo` which which exposes information about the forwarder of the  keys with which an event was encrypted if they were shared as part of an [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) room key bundle.
2025-12-19 17:29:22 +00:00
Skye Elliot 42a5910d8f feat(crypto): Introduce ForwarderData for session forwader info. (#5980)
<!-- description of the changes in this PR -->

- Introduces a new enum `ForwarderData` as a wrapper for valid variants
of `SenderData` for which we can accept key bundles under MSC4268.
- Converts `forwarder_data` in `InboundGroupSession` and
`PickledInboundGroupSession` to use `Option<ForwarderData>` over
`Option<SenderData>`.

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

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-12-19 14:51:15 +00:00
Richard van der Hoff 66daf3f7d3 Merge remote-tracking branch 'origin/main' into rav/clean_up_shield_state 2025-12-19 13:44:41 +00:00
Ivan Enderlin c3c367c54c doc(base): Update CHANGELOG.md. 2025-12-19 14:03:11 +01:00
Ivan Enderlin bf3cb6ba84 chore(sdk): Make Clippy happy :-). 2025-12-19 14:03:11 +01:00
Ivan Enderlin 5c5dcaa027 test(sdk): Add tests for new_remote and erasable values. 2025-12-19 14:03:11 +01:00
Ivan Enderlin ca64af1390 feat(sdk): When a m.room.redaction targets the current [LatestEventValue], it must be erased.
This patch implements a new feature: when a `m.room.redaction` targets
the current [`LatestEventValue`], this one must be erased by the new
computed `LatestEventValue`.
2025-12-19 14:03:11 +01:00
Ivan Enderlin ebe00841fd refactor(sdk): Replace bool by ControlFlow.
This patch replaces the `bool`s returned by `filter_*` functions by
`ControlFlow`s.
2025-12-19 14:03:11 +01:00
Ivan Enderlin 85f321f30a refactor(sdk): LatestEventValueBuilder receives the current value's event ID.
This patch spreads the current value's event ID in
`LatestEventValueBuilder`.

This patch also changes `LatestEventValueBuilder::new_remote` to return
an `Option`, similarly to `new_local`. The `must_overwrite_existing`
variable is set to `true` to keep the existing behaviour, but it's going
to change in the next patch.

This patch is purely a refactoring with no feature change. Most of the
changes are in test to keep track of the _previous value_ so that the
current value's event ID can be calculated instead of hardcoded.
2025-12-19 14:03:11 +01:00
Ivan Enderlin e2ea84f3e3 chore(sdk): Rename a variable.
This patch renames a variable. Since `rfind_map_event_id_memory_by`
returns the previous event instead of the previous event ID, this
variable must have been renamed.
2025-12-19 14:03:11 +01:00
Ivan Enderlin d5898a64ab feat(base): Add LatestEventValue::event_id.
This patch adds the `event_id` method on `LatestEventValue`, along with
the tests.
2025-12-19 14:03:11 +01:00
Ivan Enderlin 42b79d7d8a feat(base): LatestEventValue::LocalHasBeenSent gains an event_id field.
This patch adds the `event_id: OwnedEventId` field to
`LatestEventValue::LocalHasBeenSent`.
2025-12-19 14:03:11 +01:00
Ivan Enderlin 9363745fb0 chore(sdk): Remove timer! logs.
This patch removes `timer!` logs, those are no longer useful.
2025-12-19 14:03:11 +01:00
Richard van der Hoff b5f2128db1 common: remove now-unused ShieldStateCode::SentInClear 2025-12-18 13:54:49 +00:00
Richard van der Hoff d5ce01acab ui: new type for EventTimelineItem::get_shield
Separate the shield types between common and UI, so that we can change common
without breaking UI.

The new type does not include a `message` field: since it cannot be localised,
clients should not be using it.
2025-12-18 13:54:49 +00:00
Richard van der Hoff dbefaef777 bindings: remove message from ShieldState
Since this can't be localised, apps shouldn't be using it.
2025-12-18 13:09:54 +00:00
Richard van der Hoff 7438c59acd bindings: get_shields: stop returning Option
Again, there is no need for an `Option` as well as a `None` variant
2025-12-18 13:07:34 +00:00
Richard van der Hoff f5cda21d59 ui: TimelineEventItem::get_shield: stop returning Option
The `ShieldState` enum has a `None` variant, so we don't need an `Option` on
top of it.
2025-12-18 13:07:34 +00:00
Jorge Martín 759c5a9fcd docs: Add changelog entries 2025-12-18 10:41:43 +01:00
Jorge Martín 1549194b2f feat(ffi): Add an actual ffi::TimelineEventType enum with only the type
Use that for `RoomPowerLevels::events` instead.
2025-12-18 10:41:43 +01:00
Jorge Martín 4665b4343d refactor(ffi): Rename TimelineEventType to TiemlineEventContent since it also contains the event contents for some of the types 2025-12-18 10:41:43 +01:00
Jorge Martín 1f94e9d20c feat(ffi): Add fn RoomPowerLevels::events
With this we can query the power level value for any event type
2025-12-18 10:41:43 +01:00
Skye Elliot b3f6df939b Merge pull request #5943 from matrix-org/kaylendog/history_sharing/store_history_sender_details
feat(crypto): Add `forwarder_data` to `InboundGroupSession` and pickle.
2025-12-17 17:16:57 +00:00
Skye Elliot 0e568a4ee6 refactor: Use impl Iterator<Item = InboundGroupSession> as param. 2025-12-17 16:50:59 +00:00
Skye Elliot f94ce7e91c docs: Improve doc comments, linkify MSC4268. 2025-12-17 16:43:54 +00:00
Stefan Ceriu 6042bc93f6 chore(spaces): move the SpaceServices setup logic to its constructor and make it async 2025-12-17 14:42:14 +02:00
Stefan Ceriu 80be172fdf chore(ui): Move the 5955's changelog to the right position 2025-12-17 14:42:14 +02:00
Stefan Ceriu 4cc863a9fb chore(spaces): add changelogs 2025-12-17 14:42:14 +02:00
Stefan Ceriu 51e07d9fba chore(spaces): Rename subscribe_to_joined_spaces to subscribe_to_top_level_joined_spaces 2025-12-17 14:42:14 +02:00
Stefan Ceriu 143d96e300 chore(spaces): rename the SpaceService's SpaceState's joined_rooms to top_level_joined_spaces 2025-12-17 14:42:14 +02:00
Stefan Ceriu a2fd2536c4 chore(spaces): Rename joined_spaces to top_level_joined_spaces 2025-12-17 14:42:14 +02:00
Stefan Ceriu f5d751b3eb chore(spaces): Rename update_joined_spaces_if_needed to update_space_state_if_needed 2025-12-17 14:42:14 +02:00
Stefan Ceriu 096dfd61cb chore(spaces): rename joined_spaces_for to build_space_state 2025-12-17 14:42:14 +02:00
Stefan Ceriu 89f66ecd10 chore(spaces): Extract the SpaceService's subscription for rooms outside the subscribe_to_joined_spaces and make it explicit.
This will avoid having to awkwardly call `space_service.joined_spaces` without it even being needed.
2025-12-17 14:42:14 +02:00
Skye Elliot 85f07b10ad chore: Remove unused TryFrom<&HistoricRoomKey> implementation. 2025-12-17 12:33:21 +00:00
Skye Elliot 809643a159 tests(crypto): Doctests, update snapshots to include forwarder_data. 2025-12-17 12:33:21 +00:00
Richard van der Hoff f753d478fa feat: Add forwarder_data to InboundGroupSession and pickle.
- Introduces `forwarder_data` to IGS and its pickled form, and a
  helper method to import them from `HistoricRoomKey`s.

Issue: https://github.com/matrix-org/matrix-rust-sdk/issues/5109

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-12-17 12:33:16 +00:00
Damir Jelić 282a2bc8ef refactor(timeline): Don't request redecryptions in the timeline 2025-12-17 13:19:38 +01:00
Damir Jelić c6fb3c25f3 feat(r2d2): Let R2D2 attempt to update encryption info for in-memory events 2025-12-17 13:19:38 +01:00
Damir Jelić 5429106ab3 feat(r2d2): Let R2D2 attempt to redecrypt events that are in the memory of the event cache 2025-12-17 13:19:38 +01:00
Damir Jelić 5c387f2b81 refactor(r2d2): Convert the filter closure for decrypted events into function 2025-12-17 13:19:38 +01:00
Damir Jelić 6c68ca2a64 refactor(r2d2): Split out the encryption info update method into reusable components 2025-12-17 13:19:38 +01:00
Michael Goldenberg 4ee6906f47 doc(sdk): update changelog
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 46b9c11139 doc(indexeddb): update changelog
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 8e1510821b style(indexeddb): cargo fmt
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 288f28620c refactor(indexeddb): add deprecation note on open_stores_with_name()
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg aa5497e385 feat(client): initialize all stores in indexeddb store config
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 6bf121b58e refactor(indexeddb): ensure event-cache-store feature flag compiles in isolation
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 01b130a401 refactor(indexeddb): ensure media-store feature flag compiles in isolation
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 256fb0406d refactor(indexeddb): use finer-grain feature flags to include/exclude serializers
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg c57478ee18 feat(indexeddb): add media-store to default features
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg e74bf33178 feat(indexeddb): expose struct and fns for opening all stores
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 2bec882c83 feat(indexeddb): add fn to media store builder for prefixing db name
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Michael Goldenberg 2cfcc957ca feat(indexeddb): add fn to event cache store builder for prefixing db name
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-17 12:56:09 +01:00
Damir Jelić 972b3dc88b test: Add a test which showcases that redecryption for timelines with an event focus is broken (#5975)
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2025-12-17 11:38:08 +00:00
razvp ea43e3f5a8 feat(sdk): Bulk process thread subscription updates from sync and companion enpoint 2025-12-17 11:03:50 +01:00
razvp 67b1de613c feat(state-stores): Add StateStore::upsert_thread_subscriptions() method for bulk upsert 2025-12-17 11:03:50 +01:00
Jorge Martín 1af22a70b7 fix(sdk_common): TimelineEvent::from_bundled_latest_event can remove session_id
What's more, this is saved into the event cache and sometimes it overrides another instance of the same event that actually contains the right info. This results in unresolvables UTDs.

This change tries to fetch the session id from the existing event content. It's fixed these kind of UTDs when tested in a real client.
2025-12-16 16:21:51 +01:00
Damir Jelić 504d15f171 chore: Fix some spelling issues 2025-12-16 15:46:34 +01:00
Damir Jelić 1302afb844 test: Add another test for pinned timelines
This time we're testing the redecryption of pinned events that were not
part of the main timeline, more importantly we never backpaginated
enough for them to be part of the main timeline and thus never got put
into the event cache.

This test expectedly fails for now.
2025-12-16 15:46:34 +01:00
Damir Jelić 028d610397 fix: Only replace UTDs in pinned timeline with decrypted events that have the same event ID 2025-12-16 15:46:34 +01:00
Damir Jelić 7323c79dc2 test: Refactor the pinned timeline integration test a bit
This should allow us in the future to create more such tests with
many more events.
2025-12-16 15:46:34 +01:00
Damir Jelić 16c6b57c9a fix(timeline): Allow focused timelines to replace UTDs with decrypted events 2025-12-16 15:46:34 +01:00
Damir Jelić 329bdaa785 test(timeline): Add a test that pinned events get replaced after they get redecrypted 2025-12-16 15:46:34 +01:00
Ivan Enderlin e57185a009 fix(sdk): new_local returns an Option.
We can't use `LatestEventValue::None` as an optional value anymore,
since it erases the previous `LatestEventValue`. This patch updates
`LatestEventValueBuilder::new_local` to return an `Option` to handle all
the cases where a local value cannot be computed.
2025-12-16 15:42:37 +01:00
Ivan Enderlin 4e90ceae91 doc: Update the CHANGELOG.mds. 2025-12-16 13:45:00 +01:00
Ivan Enderlin ccf11ad041 feat(ui): latest_event sorter handles LatestEventValue::LocalHasBeenSent.
This patch changes the semantics of the Room List `latest_event`
sorter by changing “is local” to “is remote like”, to include the new
`LatestEventValue::LocalHasBeenSent` variant.
2025-12-16 13:45:00 +01:00
Ivan Enderlin 631671fb1c fix(sdk): Introduce LatestEventValue::LocalHasBeenSent.
The problem we are trying to solve is the following:

- a local event is being sent,
- the `LatestEventValue` is `LocalIsSending`,
- the local event is finally sent,
- the `LatestEventValue` is still `LocalIsSending` purposely, with the
  hope that an update from the Event Cache will replace it.

But sometimes, this update from the Event Cache comes **before** the
update from the Send Queue. Why is it problem? Because updates from the
Event Cache are ignored until the buffer of local `LatestEventValue`s
aren't empty, which means that if an update from the Event Cache is
received before `RoomSendQueueUpdate::SentEvent`, it is ignored, and the
`LatestEventValue` stays in the `LocalIsSending` state. That's annoying.

The idea is to introduce a new state: `LocalHasBeenSent` which mimics
`Remote`, but for a local event. It clarifies the state of a sent event,
without relying on the Event Cache.
2025-12-16 13:45:00 +01:00
Ivan Enderlin 1480ede8d4 chore(sdk): Format. 2025-12-16 12:33:04 +01:00
Ivan Enderlin e0b1f471fa doc(sdk): Document the With inner type. 2025-12-16 12:33:04 +01:00
Ivan Enderlin 58d25464c2 doc(sdk): Update CHANGELOG.md. 2025-12-16 12:33:04 +01:00
Ivan Enderlin 277bdce01d chore(sdk): Small refactoring.
This patch simplifies the code after the recent refactorings.

It uses `LatestEventValue::is_none()` to replace a `matches!`, and it
replaces the last use of `new_remote` by `new_remote_with_power_levels`
to finally rename this latter to `new_remote`.
2025-12-16 12:33:04 +01:00
Ivan Enderlin 9cf7719958 test(sdk): Fix a test on slow system. 2025-12-16 12:33:04 +01:00
Ivan Enderlin 46f313ac28 test(sdk): Simplify a test. 2025-12-16 12:33:04 +01:00
Ivan Enderlin a630904b41 perf(sdk): Do not replace a LatestEventValue::None by itself.
Replacing a `LatestEventValue::None` by a `LatestEventValue::None` is
ignored. It reduces the number of (useless) updates in the system.
2025-12-16 12:33:04 +01:00
Ivan Enderlin f80140d5ff feat(sdk): Compute LatestEventValue when initialized if None.
The `LatestEventValue` can be `None` (the default value) but the Event
Cache contains enough data to compute a `Remote(_)` one. The system
lazily triggers a `LatestEventQueueUpdate` to achieve that.
2025-12-16 12:33:04 +01:00
Ivan Enderlin a21079b2ac test(ui): Fix a test!
This `test_room_sorting` test was asserting a bug. With the last patch,
this bug is now fixed, and the test must be fixed too.
2025-12-16 12:33:04 +01:00
Ivan Enderlin f283a0aadf chore(sdk): Replace an Option<T> by OnceCell<T>. 2025-12-16 12:33:04 +01:00
Ivan Enderlin 248961fe31 feat(sdk): LatestEventValue is restored from RoomInfo.
Previously, `LatestEventValue` was always initialized from the
`RoomEventCache`. Now, it is restored from the `RoomInfo`. First off,
this is something we wanted to do since a long time. Second, it is
more performant. Third, it allows the system to be lazier. Indeed,
it's possible that when a `LatestEvent` is created, its correspond
`RoomEventCache` doesn't exist yet. It happens during a sync when a room
is new: latest events are registered, but their `RoomEventCache` aren't
yet created. By postponing the use of `RoomEventCache`, the system is
lazier and more solid.

Bonus, less methods are async, which simplifes the workflow.
2025-12-16 12:33:04 +01:00
Ivan Enderlin cdc39b69a1 fix(sdk): Remove latest_events::RoomRegistration.
This patch removes `RoomRegistration` along with the full room
registration mechanism. It's been introduced to remove contention
around the `RegisteredRooms` lock, but it actually creates more async
flows, which makes the `latest_events` logic a bit less predictable.
By removing this room registration mechanism, our hope is to make the
result more predictable and less buggy in appareance. Our real-life
tests have shown that the lock contention isn't problematic, especially
since `RoomLatestEventsReadGuard` and `RoomLatestEventsWriteGuard` have
been introduced.
2025-12-16 12:33:04 +01:00
dependabot[bot] 95dac018e3 chore(deps): bump actions/upload-artifact from 5 to 6
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 11:38:45 +01:00
dependabot[bot] 0e5077dab1 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 72602674bc341ca927683caddbf578672c352476 to 6229aa757e3e8a028bd97a49e190207e108eefbd.
- [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/72602674bc341ca927683caddbf578672c352476...6229aa757e3e8a028bd97a49e190207e108eefbd)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 11:38:15 +01:00
dependabot[bot] 3b5b0f81c4 chore(deps): bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 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/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-16 09:17:45 +01:00
dependabot[bot] a527439eae chore(deps): bump tj-actions/changed-files from 47.0.0 to 47.0.1
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 47.0.0 to 47.0.1.
- [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.0...v47.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 17:40:37 +02:00
Damir Jelić 4eb1981779 chore: Remove the dead message-ids feature 2025-12-15 14:43:52 +01:00
Damir Jelić 073f95436f refactor(timeline): Remove the backup_states_task
R2D2 now sends out a report when backups become available, so we can
just listen to that instead.
2025-12-15 14:43:43 +01:00
Damir Jelić fa1ebbfdb8 feat(r2d2): Send out a report when backups get enabled 2025-12-15 14:43:43 +01:00
Damir Jelić 9bdd2ae977 test: Ensure that the test_enabling_backups_retries_decryption test times out 2025-12-15 14:43:43 +01:00
Damir Jelić 3a63838cdb test: Disable the lease lock tests for the memory store on wasm
This test for one reason or the other sporadically panics with an:
    > RuntimeError: unreachable

Let's disable this test on Wasm for now since the memory store isn't
that relevant anyways, especially not on Wasm.
2025-12-15 13:56:11 +01:00
Damir Jelić 7a1a2202f8 feat(r2d2): Add logs for when the room key stream lags 2025-12-15 11:38:12 +01:00
Damir Jelić ce65317ab8 chore: Fix an incorrect warning due to the zeroize macro 2025-12-11 19:27:45 +01:00
Damir Jelić cd988e53f8 doc(encryption): Add a link to MSC4287 2025-12-11 19:27:45 +01:00
Jonas Platte ad58607013 Fix new clippy lints 2025-12-11 16:18:42 +01:00
Jonas Platte eae3006f8d Reformat matrix-sdk-sqlite 2025-12-11 16:18:42 +01:00
Jonas Platte 024fd99e71 Upgrade matrix-sdk-sqlite to Rust edition 2024 2025-12-11 16:18:42 +01:00
Damir Jelić 4d3125e58e fix(common): Fix the debug implementation of TimelineEventKind 2025-12-11 16:09:52 +01:00
Damir Jelić 17a4888481 chore: Convert some Note's to NOTE's
The later is more standard and some editor plugins will highlight those
for you.
2025-12-11 16:09:21 +01:00
Damir Jelić cc4cf3d54f refactor(r2d2): Create common report_lag method to reduce duplicated code 2025-12-11 13:34:48 +01:00
Mauro 9a6acd5334 Add space_room_from_id function (#5944)
Useful to get a specific space room if available given its id.

---------

Co-authored-by: Doug <douglase@element.io>
2025-12-11 12:11:48 +00:00
Damir Jelić 2522a3694f refactor(qr-login): Prepare the secure channel to be usable with HPKE
This patch abstracts away the cryptographic channel which is used in the
SecureChannel implementation for the QR code login support.

This will allow us to use HPKE alongside of ECIES since MSC4108 recently
proposed the switch to HPKE.
2025-12-11 09:24:55 +01:00
Jonas Platte f2ba338e12 Fix new rustc + clippy warnings 2025-12-10 16:37:43 +00:00
Jonas Platte 0035259e3d Reformat matrix-sdk-crypto 2025-12-10 16:37:43 +00:00
Jonas Platte 431eb88a2d Upgrade matrix-sdk-crypto to Rust edition 2024 2025-12-10 16:37:43 +00:00
Jonas Platte f8d5014921 Add + use<_> to impl Trait return types
(part of `cargo fix --edition`)
2025-12-10 16:37:43 +00:00
Jonas Platte b0072160af Remove unnecessary ref mut from patterns
(part of `cargo fix --edition`)
2025-12-10 16:37:43 +00:00
Jonas Platte 536f889649 Add a missing semicolon 2025-12-10 16:37:43 +00:00
Jonas Platte 2c4a718c1a Rename local variable for edition 2024 compatibility
`gen` is a keyword starting in edition 2024.
2025-12-10 16:37:43 +00:00
Ivan Enderlin b19377a96c fix(sdk): Better handling of redacted and redaction events in Latest Event (#5932)
This patch revisits the way redacted and redaction events are handled in
the Latest Event.

Previously, all redacted events were considered suitable candidate. It's
no longer the case.

Redaction and redacted events are no longer considered suitable.

This patch also revisits `rfind_map_event_in_memory_by` to return a
`&TimelineEvent` instead of an `OwnedEventId`, which could be more
performant in the future.

The tests have been updated accordingly.

---

* Fix https://github.com/matrix-org/matrix-rust-sdk/issues/5899
* Address https://github.com/matrix-org/matrix-rust-sdk/issues/4112

Signed-off-by: Stefan Ceriu <stefanc@matrix.org>
Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
2025-12-10 12:37:01 +00:00
Ivan Enderlin 76cae09f37 test(sdk): Test RoomEventCacheGenericUpdates are broadcasted. 2025-12-10 12:46:02 +01:00
Ivan Enderlin 81eb466555 fix(sdk): Broadcast a RoomEventCacheGenericUpdate when redecrypting events.
This patch sends a missing `RoomEventCacheGenericUpdate` when
redecrypting events.
2025-12-10 12:46:02 +01:00
Ivan Enderlin 0a24622ca2 fix(sdk): Broadcast a RoomEventCacheGenericUpdate when paginating from the network.
This patch sends a missing `RoomEventCacheGenericUpdate` when paginating
events from the network.
2025-12-10 12:46:02 +01:00
Ivan Enderlin db0f4dd31c chore: Add logs in matrix_sdk::latest_events. 2025-12-10 12:46:02 +01:00
Ivan Enderlin fbc69837e1 perf(sdk): Increase various channel capacities. 2025-12-10 12:46:02 +01:00
Ivan Enderlin b9b5ead89e perf(sdk): Use an unbounded channel for room registration. 2025-12-10 12:46:02 +01:00
Ivan Enderlin 1c0f447632 perf(sdk): Increase the capacity of the room registration channel.
This patch increases the capacity of the room registration channel. The
hope is that it can reduce the need to wait on available permits under
heavy load.
2025-12-10 12:46:02 +01:00
Ivan Enderlin e21a95f631 refactor(sdk): Do not use Fuse in listen_to_event_cache_and_send_queue_updates task.
This patch removes the use of `Fuse` in the
`listen_to_event_cache_and_send_queue_updates` task. `mpsc::Receiver`
and `broadcast::Received` are cancellation safe.
2025-12-10 12:46:02 +01:00
Ivan Enderlin 5a05bcb6b7 refactor(sdk): Hold write lock as few as possible.
This patch reduces the lifetime of the write locks in
`RegisteredRooms::room_latest_event` when the `RoomLatestEvents`
doesn't exists. If the `room_registration_sender` channel is full,
it has to wait. When waiting, the write lock is still alive, probably
blocking other operations. The idea is to create the `RoomLatestEvents`,
to downgrade the write lock to a read lock, and then to send the
registration message onto the `room_registration_sender`.
2025-12-10 12:46:02 +01:00
Hugh Nimmo-Smith 183116a4b1 Changelog 2025-12-09 19:18:06 +01:00
Hugh Nimmo-Smith e916d9d374 fix(sdk): support device IDs that aren't Curve25519 public keys in GrantLoginWithGeneratedQrCode and GrantLoginWithScannedQrCode 2025-12-09 19:18:06 +01:00
Stefan Ceriu 604ce4acfd chore(sdk, crypto): add changelogs 2025-12-09 18:49:03 +01:00
Damir Jelić cd57da59ec fix: Correctly construct the decrypted event in the send queue 2025-12-09 18:49:03 +01:00
Damir Jelić 9ff90a9b4d Add an integration test that the send queue can insert encrypted events 2025-12-09 18:49:03 +01:00
Stefan Ceriu 98f34f010c change(room::futures): Use a proper struct instead of a tuple for SendMessageLikeEvent results 2025-12-09 18:49:03 +01:00
Stefan Ceriu d513807c9f change(send_queue): Use the Room:send_raw resulting EncryptionInfo to create a DecryptedRoomEvent and corresponding TimelineEvent and correctly populate the Event Cache. 2025-12-09 18:49:03 +01:00
Stefan Ceriu 8bd401b003 change(matrix_sdk::Room): Return the used EncryptionInfo when sending MessageLike and RawMessageLike events 2025-12-09 18:49:03 +01:00
Stefan Ceriu b7d22da9f0 change(crypto): provide encryption information back directly from the Olm machine's raw encryption methods 2025-12-09 18:49:03 +01:00
Hugh Nimmo-Smith b98a832c67 feat(ffi): Add QrCodeData::to_bytes() to allow generation of a QR code (#5939)
Signed-off-by: Hugh Nimmo-Smith <hughns@element.io>
2025-12-09 17:00:44 +00:00
Michael Goldenberg acf3a7a04b doc(indexeddb): update changelog
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-09 15:24:15 +01:00
Michael Goldenberg 790136b9db fix(indexeddb): skip encoding event id when constructing bounds
In the implementation of EventCacheStore, there are a number of
places where the upper and lower bounds of an EventId are
constructed. It is important to bypass hashing and encryption
when constructing these bounds, otherwise the values will be
modified and will no longer represent the bounds.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-09 15:24:15 +01:00
Michael Goldenberg 265cfc7710 test(indexeddb): initialize store cipher in encrypted tests for event cache and media store
Note that the encrypted tests were actually being run unencrypted.
Introducing a store cipher causes them to run encrypted, and
furthermore, reveals some bugs which are only visible when running
an encrypted event cache store.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-12-09 15:24:15 +01:00
Damir Jelić 29b693e625 chore: Update the changelog to include the CVE and GSA references 2025-12-08 12:45:42 +01:00
Johannes Marbach 8637bdce12 fixup! feat(room): make load_event_with_relations also load relations when falling back to the network
Try loading relations from the cache before falling back to the server

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-12-08 09:44:59 +00:00
Johannes Marbach db4e1b2c00 fixup! feat(room): make load_event_with_relations also load relations when falling back to the network
Cache related events after loading them

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-12-08 09:44:59 +00:00
Johannes Marbach c82e1b8ec3 fixup! feat(room): make load_event_with_relations also load relations when falling back to the network
Change limit to 256

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-12-08 09:44:59 +00:00
Johannes Marbach 9105db3d82 feat(room): make load_event_with_relations also load relations when falling back to the network
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-12-08 09:44:59 +00:00
JoFrost 465300560e feat[bindings]: move into specific structures the threshold and add previous values 2025-12-05 12:07:43 +02:00
JoFrost c97ab27052 chore[bindings]: clippy 2025-12-05 11:21:56 +02:00
JoFrost b776ae9090 feat[bindings]: expose power level thresholds in corresponding timeline event 2025-12-05 11:05:56 +02:00
Ivan Enderlin 238e4e8a87 doc: Mention #5624 in CHANGELOG.mds. 2025-12-05 09:40:47 +01:00
Ivan Enderlin 37c516f7f9 refactor: Rename new_latest_event to latest_event.
This patch removes the “new_” prefix to the latest event API.
2025-12-05 09:40:47 +01:00
Ivan Enderlin 81a8aa063b chore(base): Remove the old latest event API. 2025-12-05 09:40:47 +01:00
Ivan Enderlin 91091c7819 chore(ui): Remove the old latest event API.
So satisfying.
2025-12-05 09:40:47 +01:00
Ivan Enderlin e4141b216a chore(ffi): Remove the old latest event API. 2025-12-05 09:40:47 +01:00
Kévin Commaille b0574c1c2c feat(sdk): Add support for the stable m.oauth UIAA type
By replacing the custom implementation with the one in Ruma.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-05 09:20:48 +01:00
Jorge Martín bfd8cfa1c8 refactor: Add uniffi-rs entries to deny.toml 2025-12-04 15:17:26 +01:00
Jorge Martín 67815e7787 refactor: Remove unused cargo_metadata dependency 2025-12-04 15:17:26 +01:00
Jorge Martín 031d8457b6 refactor: Upgrade uniffi to the latest upstream 2025-12-04 15:17:26 +01:00
Ivan Enderlin c84f5965c3 test(ui): Fix tests according to last change.
This path adds events or state events to force the test to execute as
expected since a change in `bump_stamp` alone doesn't trigger a room
list update anymore.
2025-12-04 13:39:01 +01:00
Ivan Enderlin 638028ba7c fix(ui): Reduce number of updates in the room list.
This patch fixes a “bug” in the Room List. It's updated by
`RoomInfoNotableUpdateReasons`. However, 1 reason is creating
unnecessary updates: `RECENCY_STAMP`. The Room List is already updated
by the Latest Event. One usage of the Latest Event is to sort the Room
List by recency. Thus, since the Room List is updated by `LATEST_EVENT`,
we can ignore `RECENCY_STAMP`.
2025-12-04 13:39:01 +01:00
Ivan Enderlin ae9600e872 doc(sdk): Remove mention of #3941.
This patch removes the mention of #3941 now it's closed.
2025-12-04 12:06:24 +01:00
Damir Jelić b5d5a41453 Merge pull request #5926 from matrix-org/poljar/release-0.16.0
Release prep for 0.16.0
2025-12-04 10:32:26 +01:00
Doug 93f71ba977 ffi: Add support for checking login with QR code availability. 2025-12-03 18:35:29 +02:00
541 changed files with 49645 additions and 26950 deletions
+1 -1
View File
@@ -52,5 +52,5 @@ allow-git = [
# on releases.
"https://github.com/matrix-org/vodozemac",
# A patch override for the bindings: https://github.com/Alorel/rust-indexed-db/pull/72
"https://github.com/matrix-org/rust-indexed-db",
"https://github.com/matrix-org/rust-indexed-db"
]
+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 -->
+8 -4
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,7 +82,9 @@ 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
@@ -92,8 +97,7 @@ jobs:
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
- name: Run the benchmarks
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad
uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537
with:
run: cargo codspeed run
mode: "instrumentation"
token: ${{ secrets.CODSPEED_TOKEN }}
mode: simulation
+34 -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,17 @@ 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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # 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 +51,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -69,32 +73,35 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # 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 +109,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -136,16 +143,18 @@ 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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Install aarch64-apple-ios target
run: rustup target install aarch64-apple-ios
@@ -156,12 +165,12 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
@@ -179,7 +188,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 +200,18 @@ 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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Add rust targets
run: |
@@ -212,7 +223,7 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
+58 -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,7 @@ jobs:
- no-encryption-and-sqlite
- sqlite-cryptostore
- experimental-encrypted-state-events
- rustls-tls
- native-tls
- markdown
- socks
- sso-login
@@ -43,10 +45,12 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Install libsqlite
run: |
@@ -54,7 +58,7 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
@@ -65,10 +69,10 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -85,21 +89,23 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -116,7 +122,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 +132,20 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
components: clippy
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -169,10 +177,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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
with:
tool: protoc@3.20.3
@@ -183,17 +193,17 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
with:
toolchain: ${{ matrix.rust }}
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Test
run: |
@@ -241,22 +251,24 @@ 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@631a55b12751854ce901bb631d5902ceb48146f7 # stable
with:
targets: wasm32-unknown-unknown
components: clippy
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.2
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@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
# use a separate cache for each job to work around
# https://github.com/Swatinem/rust-cache/issues/124
@@ -267,10 +279,10 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -291,10 +303,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.40.0
uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0
lint:
name: Lint
@@ -303,26 +317,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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
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@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -362,7 +378,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 +388,15 @@ jobs:
sudo apt-get install libsqlite3-dev
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Test
env:
@@ -390,7 +408,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 }}
+10 -7
View File
@@ -6,6 +6,8 @@ on:
pull_request:
branches: [main]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -97,9 +99,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 +112,7 @@ jobs:
sudo rm -rf /var/lib/apt/lists/*
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # 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 +120,19 @@ jobs:
run: rm .cargo/config.toml
- name: Load cache
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
with:
prefix-key: "coverage"
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
uses: taiki-e/install-action@8a8ecf49de4feac160d13668bc406fe17413c1bf # cargo-llvm-cov
- name: Install nextest
uses: taiki-e/install-action@nextest
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -167,7 +170,7 @@ jobs:
# 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@v5
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: codecov_report
path: |
+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
+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.0
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@72602674bc341ca927683caddbf578672c352476
uses: bnjbvr/cargo-machete@2e2703d664c176156a22936b9203c38cc7a273dd
+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@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
with:
tool: protoc@3.20.3
- name: Install Rust
uses: dtolnay/rust-toolchain@master
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
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@c676846f29d98ff6b0106d3608c7ffd4048af17b # v2
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@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
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@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
+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
+7 -3
View File
@@ -7,10 +7,14 @@ 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@4146cc238a18f9b3f3427572fd4585d2d2d78c88 # cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets
+7 -4
View File
@@ -10,6 +10,8 @@ on:
types:
- completed
permissions: {}
jobs:
coverage:
name: Upload coverage report
@@ -18,7 +20,7 @@ jobs:
steps:
- name: 'Fetch coverage report from artifacts'
id: prepare_report
uses: actions/github-script@v8
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
var fs = require('fs');
@@ -58,13 +60,14 @@ jobs:
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
persist-credentials: false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
@@ -79,7 +82,7 @@ jobs:
directory: ${{ github.workspace }}
- name: Upload test results to Codecov
uses: codecov/test-results-action@v1
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1
with:
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
fail_ci_if_error: true
+7 -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@v4
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
id: xtask-cache
with:
path: target/debug/xtask
@@ -64,7 +68,7 @@ jobs:
- name: Install Rust stable toolchain
if: steps.xtask-cache.outputs.cache-hit != 'true'
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
- name: Build
if: steps.xtask-cache.outputs.cache-hit != 'true'
+6 -1
View File
@@ -16,12 +16,17 @@ extend-ignore-re = [
[default.extend-identifiers]
WeeChat = "WeeChat"
# all of these are valid words, but should never appear in this repo
[default.extend-words]
# all of these are valid words, but should never appear in this repo
bellow = "below"
stat = "state"
sing = "sign"
singed = "signed"
singing = "signing"
# crate name
ratatui = "ratatui"
# path name
consts = "consts"
# base64 false positives
Nd = "Nd"
Abl = "Abl"
Generated
+497 -298
View File
File diff suppressed because it is too large Load Diff
+108 -73
View File
@@ -11,63 +11,70 @@ 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 = "1.0.100"
aquamarine = "0.6.0"
as_variant = "1.3.0"
assert-json-diff = "2.0.2"
assert_matches = "1.5.0"
assert_matches2 = "0.1.2"
async-compat = "0.2.5"
async-rx = "0.1.3"
anyhow = { version = "1.0.100", default-features = false }
aquamarine = { version = "0.6.0", default-features = false }
as_variant = { version = "1.3.0", default-features = false }
assert-json-diff = { version = "2.0.2", default-features = false }
assert_matches = { version = "1.5.0", default-features = false }
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 }
# 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 = "0.3.5"
async-trait = "0.1.89"
base64 = "0.22.1"
bitflags = "2.10.0"
byteorder = "1.5.0"
cfg-if = "1.0.4"
clap = "4.5.53"
chrono = "0.4.42"
dirs = "6.0.0"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.8.0", features = ["tracing"] }
eyeball-im-util = "0.10.0"
futures-core = "0.3.31"
futures-executor = "0.3.31"
futures-util = "0.3.31"
getrandom = { version = "0.2.15", default-features = false }
gloo-timers = "0.3.0"
growable-bloom-filter = "2.1.1"
hkdf = "0.12.4"
hmac = "0.12.1"
http = "1.3.1"
imbl = "6.1.0"
indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures" }
indexmap = "2.12.1"
async-stream = { version = "0.3.6", default-features = false }
async-trait = { version = "0.1.89", default-features = false }
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
bitflags = { version = "2.10.0", default-features = false }
byteorder = { version = "1.5.0", default-features = false, features = ["std"] }
cfg-if = { version = "1.0.4", default-features = false }
clap = { version = "4.5.53", default-features = false, features = ["std", "help", "usage"] }
chrono = { version = "0.4.42", default-features = false, features = ["clock", "std", "oldtime", "wasmbind"] }
dirs = { version = "6.0.0", default-features = false }
eyeball = { version = "0.8.8", default-features = false, features = ["tracing"] }
eyeball-im = { version = "0.8.0", default-features = false, features = ["tracing"] }
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.3.4", 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 }
hkdf = { version = "0.12.4", default-features = false }
hmac = { version = "0.12.1", default-features = false }
http = { version = "1.3.1", default-features = false }
imbl = { version = "6.1.0", default-features = false }
indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures", default-features = false }
indexmap = { version = "2.12.1", default-features = false }
insta = { version = "1.44.1", features = ["json", "redactions"] }
itertools = "0.14.0"
js-sys = "0.3.82"
mime = "0.3.17"
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
once_cell = "1.21.3"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.16"
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
rand = "0.8.5"
regex = "1.12.2"
reqwest = { version = "0.12.24", default-features = false }
rmp-serde = "1.3.0"
ruma = { version = "0.14.0", features = [
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 = ["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.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"] }
regex = { version = "1.12.2", default-features = false }
reqwest = { version = "0.13.1", default-features = false }
ring = { version = "0.17.14", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "dc36ccf119c065b75a9872ec5a6487d680289dc1", features = [
"client-api-c",
"compat-unset-avatar",
"compat-upload-signatures",
"compat-arbitrary-length-ids",
"compat-tag-info",
@@ -76,6 +83,7 @@ ruma = { version = "0.14.0", features = [
"compat-lax-room-topic-deser",
"unstable-msc3230",
"unstable-msc3401",
"unstable-msc3417",
"unstable-msc3488",
"unstable-msc3489",
"unstable-msc4075",
@@ -88,34 +96,38 @@ ruma = { version = "0.14.0", features = [
"unstable-msc4308",
"unstable-msc4310",
] }
rustls = { version = "0.23.37", default-features = false, features = ["ring"] }
rustls-pki-types = { version = "1.14.0", default-features = false }
sentry = { version = "0.46.0", default-features = false }
sentry-tracing = "0.46.0"
serde = { version = "1.0.228", features = ["rc"] }
serde_html_form = "0.2.8"
serde_json = "1.0.145"
sha2 = "0.10.9"
similar-asserts = "1.7.0"
stream_assert = "0.1.1"
tempfile = "3.23.0"
thiserror = "2.0.17"
sentry-tracing = { version = "0.46.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"] }
sha2 = { version = "0.10.9", default-features = false }
similar-asserts = { version = "1.7.0", default-features = false }
stream_assert = { version = "0.1.1", default-features = false }
syn = { version = "2.0.43", default-features = false, features = ["derive", "parsing", "printing", "clone-impls"] }
tempfile = { version = "3.23.0", default-features = false }
thiserror = { version = "2.0.17", default-features = false }
tokio = { version = "1.48.0", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
tokio-stream = { version = "0.1.17", default-features = false }
tracing = { version = "0.1.41", default-features = false, features = ["std"] }
tracing-appender = "0.2.3"
tracing-core = "0.1.34"
tracing-subscriber = "0.3.20"
unicode-normalization = "0.1.25"
uniffi = { version = "0.30.0" }
uniffi_bindgen = { version = "0.30.0" }
url = "2.5.7"
uuid = "1.18.1"
vergen-gitcl = "1.0.8"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.105"
wasm-bindgen-test = "0.3.55"
web-sys = "0.3.82"
wiremock = "0.6.5"
zeroize = "1.8.2"
tracing-appender = { version = "0.2.3", default-features = false }
tracing-core = { version = "0.1.34", default-features = false }
tracing-subscriber = { version = "0.3.20", default-features = false, features = ["std", "smallvec", "fmt"] }
unicode-normalization = { version = "0.1.25", default-features = false }
unicode-segmentation = { version = "1.12.0", default-features = false }
uniffi = { version = "0.31.0", default-features = false, features = ["cargo-metadata"] }
uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["cargo-metadata"] }
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"] }
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 }
wiremock = { version = "0.6.5", default-features = false }
zeroize = { version = "1.8.2", default-features = false }
matrix-sdk = { path = "crates/matrix-sdk", version = "0.16.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.16.0" }
@@ -167,6 +179,19 @@ unused_async = "warn"
# Saves a lot of disk space. If symbols are needed, use the dbg profile.
debug = 0
# Profile for debug builds with full optimization and minimal debug symbols.
# This should be just enough to have proper backtraces, having way smaller binaries
# (10% of the size with full debug symbols profile, like `reldbg`).
# This profile differs from `reldbg` in not containing the debug symbols needed for
# debugging with LLDB/GDB, trading that for binary size, allowing quick iterations
# of building the bindings, installing in a real device, testing your changes, repeat.
# It's also different from `dev` in having enough debug symbols to display backtraces.
[profile.reldev]
inherits = "dev"
opt-level = 3
debug = "line-tables-only"
strip = "debuginfo"
[profile.dev.package]
# Optimize quote even in debug mode. Speeds up proc-macros enough to account
# for the extra time of optimizing it for a clean build of matrix-sdk-ffi.
@@ -187,6 +212,16 @@ debug = 2
inherits = "dbg"
opt-level = 3
[profile.dist]
# Use release profile as a base
inherits = "release"
# Strip the minimal debug info, while still allowing us to have proper backtraces, but it will affect debuggers
strip = "debuginfo"
# Use link time optimizations
lto = true
# Use binary size optimization, since this is intended for distributed copies of the SDK
opt-level = "s"
[profile.profiling]
inherits = "release"
# LTO is too slow to compile.
+3 -3
View File
@@ -15,9 +15,9 @@ codspeed = []
[dependencies]
assert_matches.workspace = true
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
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 = ["rustls-tls", "e2e-encryption", "sqlite", "testing"] }
matrix-sdk-base.workspace = true
matrix-sdk-crypto.workspace = true
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
@@ -28,7 +28,7 @@ ruma.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio = { workspace = true, default-features = false, features = ["rt-multi-thread"] }
tokio = { workspace = true, features = ["rt-multi-thread"] }
wiremock.workspace = true
[[bench]]
+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;
}
+22 -33
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;
@@ -188,10 +180,7 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
.unwrap();
let timeline = TimelineBuilder::new(&room)
.with_focus(TimelineFocus::PinnedEvents {
max_events_to_load: 100,
max_concurrent_requests: 10,
})
.with_focus(TimelineFocus::PinnedEvents)
.build()
.await
.expect("Could not create timeline");
+17 -6
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 ruma::{OwnedRoomId, RoomId, owned_user_id};
use tokio::runtime::Builder;
/// Benchmark the time it takes to create a room list.
@@ -30,15 +30,26 @@ pub fn create(c: &mut Criterion) {
let server_ts_range = Uniform::from(100..1000);
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 {
+3 -3
View File
@@ -51,6 +51,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
[dependencies.js_int]
version = "0.2.2"
default-features = false
features = ["lax_deserialize"]
[dependencies.matrix-sdk-crypto]
@@ -63,12 +64,11 @@ features = ["crypto-store"]
[dependencies.tokio]
workspace = true
default-features = false
features = ["rt-multi-thread"]
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
vergen-gitcl = { workspace = true, features = ["build"] }
uniffi = { workspace = true, default-features = false, features = ["build"] }
vergen-gitcl = { workspace = true, default-features = false, features = ["build"] }
[dev-dependencies]
assert_matches2.workspace = true
@@ -76,6 +76,49 @@ pub enum DecryptionError {
Store { error: String },
}
/// Error describing what went wrong when exporting a [`SecretsBundle`].
///
/// The [`SecretsBundle`] can only be exported if we have all cross-signing
/// private keys in the store.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum SecretsBundleExportError {
/// The store itself had an error.
#[error(transparent)]
CryptoStore(CryptoStoreError),
/// We're missing one or more cross-signing keys.
#[error("The store doesn't contain all the cross-signing keys")]
MissingCrossSigningKeys,
/// We have a backup key stored, but we don't know the version of the
/// backup.
#[error("The store contains a backup key, but no backup version")]
MissingBackupVersion,
#[error("serialization error: {error}")]
Serialization { error: String },
}
impl From<matrix_sdk_crypto::store::SecretsBundleExportError> for SecretsBundleExportError {
fn from(value: matrix_sdk_crypto::store::SecretsBundleExportError) -> Self {
match value {
matrix_sdk_crypto::store::SecretsBundleExportError::Store(e) => {
Self::CryptoStore(e.into())
}
matrix_sdk_crypto::store::SecretsBundleExportError::MissingCrossSigningKey(_)
| matrix_sdk_crypto::store::SecretsBundleExportError::MissingCrossSigningKeys => {
Self::MissingCrossSigningKeys
}
matrix_sdk_crypto::store::SecretsBundleExportError::MissingBackupVersion => {
Self::MissingBackupVersion
}
}
}
}
impl From<serde_json::Error> for SecretsBundleExportError {
fn from(err: serde_json::Error) -> Self {
Self::Serialization { error: err.to_string() }
}
}
impl From<MegolmError> for DecryptionError {
fn from(value: MegolmError) -> Self {
match &value {
@@ -506,6 +506,7 @@ fn collect_sessions(
})
.collect::<anyhow::Result<_>>()?,
sender_data: SenderData::legacy(),
forwarder_data: None,
room_id: RoomId::parse(session.room_id)?,
imported: session.imported,
backed_up: session.backed_up,
+24 -3
View File
@@ -53,7 +53,10 @@ use zeroize::Zeroize;
use crate::{
dehydrated_devices::DehydratedDevices,
error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError},
error::{
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
SignatureError,
},
parse_user_id,
responses::{response_from_string, OwnedResponse},
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
@@ -802,12 +805,12 @@ impl OlmMachine {
let room_id = RoomId::parse(room_id)?;
let content = serde_json::from_str(&content)?;
let encrypted_content = self
let result = self
.runtime
.block_on(self.inner.encrypt_room_event_raw(&room_id, &event_type, &content))
.expect("Encrypting an event produced an error");
Ok(serde_json::to_string(&encrypted_content)?)
Ok(serde_json::to_string(&result.content)?)
}
/// Encrypt the given event with the given type and content for the given
@@ -1409,6 +1412,24 @@ impl OlmMachine {
Ok(())
}
/// Export all the secrets we have in the store into a serialized
/// SecretsBundle.
///
/// This method will export all the private cross-signing keys and, if
/// available, the private part of a backup key and its accompanying
/// version.
///
/// The method will fail if we don't have all three private cross-signing
/// keys available.
///
/// **Warning**: Only export this and share it with a trusted recipient,
/// i.e. if an existing device is sharing this with a new device.
pub fn export_secrets_bundle(&self) -> Result<String, SecretsBundleExportError> {
let bundle = self.runtime.block_on(self.inner.store().export_secrets_bundle())?;
Ok(serde_json::to_string(&bundle)?)
}
/// Request missing local secrets from our devices (cross signing private
/// keys, megolm backup). This will ask the sdk to create outgoing
/// request to get the missing secrets.
@@ -209,7 +209,7 @@ impl Sas {
///
/// # Flowchart
///
/// The flow of the verification process is pictured bellow. Please note
/// The flow of the verification process is pictured below. Please note
/// that the process can be cancelled at each step of the process.
/// Either side can cancel the process.
///
+3 -3
View File
@@ -17,9 +17,9 @@ test = false
doctest = false
[dependencies]
proc-macro2 = "1.0.86"
quote = "1.0.18"
syn = { version = "2.0.43", features = ["full", "extra-traits"] }
proc-macro2 = { workspace = true , features = ["proc-macro"] }
quote.workspace = true
syn = { workspace = true, features = ["full", "extra-traits", "proc-macro"] }
[lints]
workspace = true
+151
View File
@@ -6,6 +6,157 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
### Bug Fixes
- 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
[this issue](https://github.com/mozilla/uniffi-rs/issues/2740)).
([#6069](https://github.com/matrix-org/matrix-rust-sdk/pull/6069),
[#6112](https://github.com/matrix-org/matrix-rust-sdk/pull/6112),
[#6115](https://github.com/matrix-org/matrix-rust-sdk/pull/6115),
[#6116](https://github.com/matrix-org/matrix-rust-sdk/pull/6116)).
- `Client::create_room` now uses `RoomPowerLevelsContentOverride` under the hood instead of
`RoomPowerLevelsEventContent` to be able to explicitly set values which would previously be
ignored if they matched the default power level values specified by the spec: these may not be
the same in the homeserver and result in rooms with incorrect power levels being created.
([#6034](https://github.com/matrix-org/matrix-rust-sdk/pull/6034))
- Fix the `is_last_admin` check in `LeaveSpaceRoom` since it was not
accounting for the membership state.
[#6032](https://github.com/matrix-org/matrix-rust-sdk/pull/6032)
- [**breaking**] `LatestEventValue::Local { is_sending: bool }` is replaced
by [`state: LatestEventValueLocalState`] to represent 3 states: `IsSending`,
`HasBeenSent` and `CannotBeSent`.
([#5968](https://github.com/matrix-org/matrix-rust-sdk/pull/5968/))
### Features
- [**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.
[#6050](https://github.com/matrix-org/matrix-rust-sdk/pull/6050)
- [**breaking**] Remove `TimelineFilter::EventTypeFilter` which has been replaced by
the more generic `TimelineFilter::EventFilter`. Users of `TimelineEventTypeFilter::include`
and `TimelineEventTypeFilter::exclude` can switch to `TimelineEventFilter::include_event_types`
and `TimelineEventFilter::exclude_event_types`.
([#6070](https://github.com/matrix-org/matrix-rust-sdk/pull/6070/))
- Add `TimelineFilter::EventFilter` for filtering events based on their type or
content. For content filtering, only membership and profile change filters
are available as of now.
([#6048](https://github.com/matrix-org/matrix-rust-sdk/pull/6048/))
- Introduce `SpaceFilter`s as a mechanism for narrowing down what's displayed in
the room list ([#6025](https://github.com/matrix-org/matrix-rust-sdk/pull/6025))
- Expose room power level thresholds in `OtherState::RoomPowerLevels` (ban, kick, invite, redact, state &
events defaults, per-event overrides, notifications), so clients can compute the required power level
for actions and compare with previous values. ([#5931](https://github.com/matrix-org/matrix-rust-sdk/pull/5931))
- Add `RoomCreationParameters::is_space` parameter to be able to create spaces. ([#6010](https://github.com/matrix-org/matrix-rust-sdk/pull/6010/))
- [**breaking**] `LazyTimelineItemProvider::get_shields` no longer returns an
an `Option`: the `ShieldState` type contains a `None` variant, so the
`Option` was redundant. The `message` field has also been removed: since there
was no way to localise the returned string, applications should not be using it.
([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959))
- Add `Room::list_threads` to list all the threads in a room.
([#5953](https://github.com/matrix-org/matrix-rust-sdk/pull/5953))
- Add `SpaceService::get_space_room` to get a space given its id from the space graph if available.
[#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944)
- Add `QrCodeData::to_bytes()` to allow generation of a QR code.
([#5939](https://github.com/matrix-org/matrix-rust-sdk/pull/5939))
- [**breaking**]: The new Latest Event API replaces the old API.
`Room::new_latest_event` overwrites the `Room::latest_event` method. See the
documentation of `matrix_sdk::latest_event` to learn about the new API.
[#5624](https://github.com/matrix-org/matrix-rust-sdk/pull/5624/)
- Created `RoomPowerLevels::events` function which returns a `HashMap<TimelineEventType, i64>` with all the power
levels per event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
- Expose `EventTimelineItem::forwarder` and `forwarder_profile`, which, if present, provide the ID and profile of
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 `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
- `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))
- [**breaking**] The existing `TimelineEventType` was renamed to `TimelineEventContent`, because it contained the
actual contents of the event. Then, we created a new `TimelineEventType` enum that actually contains *just* the
event type. ([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
- [**breaking**] The function `TimelineEvent::event_type` is now `TimelineEvent::content`.
([#5937](https://github.com/matrix-org/matrix-rust-sdk/pull/5937))
- [**breaking**] The `SpaceService` will no longer auto-subscribe to required
client events when invoking the `subscribe_to_joined_spaces` but instead do it
through its, now async, constructor.
([#5972](https://github.com/matrix-org/matrix-rust-sdk/pull/5972))
- [**breaking**] The `SpaceService`'s `joined_spaces` method has been renamed
`top_level_joined_spaces` and `subscribe_to_joined_spaces` to `space_service.subscribe_to_top_level_joined_spaces`
([#5972](https://github.com/matrix-org/matrix-rust-sdk/pull/5972))
## [0.16.0] - 2025-12-04
### Breaking changes
+42 -9
View File
@@ -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", "rustls-tls"]
# Use SQLite for the session storage.
sqlite = ["matrix-sdk/sqlite"]
# Use an embedded version of SQLite.
@@ -45,11 +45,12 @@ experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-em
[dependencies]
anyhow.workspace = true
chrono.workspace = true
extension-trait = "1.0.2"
eyeball-im.workspace = true
futures-util.workspace = true
language-tags = "0.3.2"
log-panics = { version = "2.1.0", features = ["with-backtrace"] }
log-panics = { version = "2.1.0", default-features = false, features = ["with-backtrace"] }
matrix-sdk = { workspace = true, features = [
"anyhow",
"e2e-encryption",
@@ -63,15 +64,38 @@ matrix-sdk-base.workspace = true
matrix-sdk-common.workspace = true
matrix-sdk-ffi-macros.workspace = true
matrix-sdk-ui = { workspace = true, features = ["uniffi"] }
mime = "0.3.17"
once_cell.workspace = true
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278"] }
mime = { version = "0.3.17", default-features = false }
ruma = { workspace = true, features = [
"html",
"unstable-msc3488",
"compat-unset-avatar",
"unstable-msc3245-v1-compat",
"unstable-msc4278",
"unstable-msc3230",
# Audio event type
"unstable-msc3927",
# File event type
"unstable-msc3551",
# Image event type
"unstable-msc3552",
# Video event type
"unstable-msc3553",
# Voice event type
"unstable-msc3245",
# Emote event type
"unstable-msc3954",
# Image pack event type
"unstable-msc2545",
# Room language event type
"unstable-msc4334",
] }
serde.workspace = true
serde_json.workspace = true
sentry = { workspace = true, optional = true, default-features = false, features = [
sentry = { workspace = true, optional = true, features = [
# Most default features enabled otherwise.
"backtrace",
"contexts",
"debug-images",
"panic",
"reqwest",
"sentry-debug-images",
@@ -83,12 +107,12 @@ tracing-appender.workspace = true
tracing-core.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
url.workspace = true
uuid = { version = "1.4.1", features = ["v4"] }
uuid = { version = "1.4.1", default-features = false, features = ["std", "v4"] }
zeroize.workspace = true
oauth2.workspace = true
[target.'cfg(target_family = "wasm")'.dependencies]
console_error_panic_hook = "0.1.7"
console_error_panic_hook = { version = "0.1.7", default-features = false }
tokio = { workspace = true, features = ["sync", "macros"] }
uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] }
futures-executor.workspace = true
@@ -99,10 +123,19 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = "0.2.2"
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"
# Used to avoid having to call an exposed JNI call from the Android client to initialize `rustls-platform-verifier`
libloading = "0.9.0"
# Needed for intializing and keeping the JavaVM reference around
once_cell = "1.21.4"
[dev-dependencies]
similar-asserts.workspace = true
tempfile.workspace = true
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }
+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,
+15 -1
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},
@@ -85,7 +99,7 @@ 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
+275 -111
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,
@@ -13,10 +27,9 @@ use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
#[cfg(feature = "sqlite")]
use matrix_sdk::STATE_STORE_DATABASE_NAME;
use matrix_sdk::{
authentication::oauth::{
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
},
authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession},
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
executor::AbortOnDrop,
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
ruma::{
api::client::{
@@ -41,9 +54,12 @@ use matrix_sdk::{
},
sliding_sync::Version as SdkSlidingSyncVersion,
store::RoomLoadSettings as SdkRoomLoadSettings,
task_monitor::BackgroundTaskFailureReason,
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
};
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_common::{
cross_process_lock::CrossProcessLockConfig, stream::StreamExt, SendOutsideWasm, SyncOutsideWasm,
};
use matrix_sdk_ui::{
notification_client::{
NotificationClient as MatrixNotificationClient,
@@ -57,8 +73,12 @@ use oauth2::Scope;
use ruma::{
api::client::{
alias::get_alias,
discovery::get_authorization_server_metadata::v1::{
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
},
error::ErrorKind,
profile::{AvatarUrl, DisplayName},
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
uiaa::UserIdentifier,
},
events::{
@@ -75,7 +95,6 @@ use ruma::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
message::{OriginalSyncRoomMessageEvent, Relation},
power_levels::RoomPowerLevelsEventContent,
},
secret_storage::{
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
@@ -86,7 +105,7 @@ use ruma::{
RoomAccountDataEvent as RumaRoomAccountDataEvent,
},
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
room_version_rules::AuthorizationRules,
room::RoomType,
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
};
use serde::{Deserialize, Serialize};
@@ -187,7 +206,19 @@ impl From<PushFormat> for RumaPushFormat {
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait ClientDelegate: SyncOutsideWasm + SendOutsideWasm {
/// A callback invoked whenever the SDK runs into an unknown token error.
fn did_receive_auth_error(&self, is_soft_logout: bool);
/// A callback invoked when a background task registered with the client's
/// task monitor encounters an error.
///
/// Can default to an empty implementation, if the embedder doesn't care
/// about handling background jobs errors.
fn on_background_task_error_report(
&self,
task_name: String,
error: BackgroundTaskFailureReason,
);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
@@ -223,6 +254,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 {
@@ -255,11 +312,20 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
}
}
struct ClientDelegateData {
/// The delegate itself, that will receive the callbacks.
delegate: Arc<dyn ClientDelegate>,
// The background task error listener task, that will forward errors occurring in background
// jobs to the delegate.
_background_error_listener_task: Arc<AbortOnDrop<()>>,
}
#[derive(uniffi::Object)]
pub struct Client {
pub(crate) inner: AsyncRuntimeDropped<MatrixClient>,
delegate: OnceLock<Arc<dyn ClientDelegate>>,
delegate_data: OnceLock<ClientDelegateData>,
pub(crate) utd_hook_manager: OnceLock<Arc<UtdHookManager>>,
@@ -276,7 +342,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> {
@@ -308,29 +373,26 @@ impl Client {
}
});
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()),
delegate: OnceLock::new(),
delegate_data: OnceLock::new(),
utd_hook_manager: OnceLock::new(),
session_verification_controller,
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 {
@@ -725,6 +787,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
@@ -882,46 +966,50 @@ impl Client {
.collect();
// Convert SDK event to FFI type
let (sender, event, thread_id) = match notification.event {
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => 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())
}
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,
}
}
}
_ => None,
};
let event = NotificationEvent::Timeline {
event: Arc::new(crate::event::TimelineEvent(Box::new(
deserialized,
))),
};
(sender, event, thread_id)
_ => 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;
}
}
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)
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!(
@@ -984,6 +1072,7 @@ impl Client {
listener.on_notification(
NotificationItem {
event,
raw_event,
sender_info,
room_info,
is_noisy: Some(is_noisy),
@@ -1131,14 +1220,14 @@ impl Client {
self: Arc<Self>,
delegate: Option<Box<dyn ClientDelegate>>,
) -> Result<Option<Arc<TaskHandle>>, ClientError> {
if self.delegate.get().is_some() {
if self.delegate_data.get().is_some() {
return Err(ClientError::Generic {
msg: "Delegate already initialized".to_owned(),
details: None,
});
}
Ok(delegate.map(|delegate| {
let handle = delegate.map(|delegate| {
let mut session_change_receiver = self.inner.subscribe_to_session_changes();
let client_clone = self.clone();
let session_change_task = get_runtime_handle().spawn(async move {
@@ -1154,13 +1243,29 @@ impl Client {
}
});
self.delegate.get_or_init(|| Arc::from(delegate));
let delegate: Arc<dyn ClientDelegate> = delegate.into();
let client = self.inner.clone();
let delegate_clone = delegate.clone();
let task = Arc::new(AbortOnDrop::new(get_runtime_handle().spawn(async move {
let mut receiver = client.task_monitor().subscribe();
while let Ok(error) = receiver.recv().await {
delegate_clone.on_background_task_error_report(error.task.name, error.reason);
}
})));
let delegate_data =
ClientDelegateData { delegate, _background_error_listener_task: task };
self.delegate_data.get_or_init(|| delegate_data);
Arc::new(TaskHandle::new(session_change_task))
}))
});
Ok(handle)
}
/// Sets the [UnableToDecryptDelegate] which will inform about UTDs.
/// Sets the [`UnableToDecryptDelegate`] which will inform about UTDs.
/// Returns an error if the delegate was already set.
pub async fn set_utd_delegate(
self: Arc<Self>,
@@ -1206,20 +1311,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> {
@@ -1505,8 +1610,8 @@ impl Client {
SyncServiceBuilder::new((*self.inner).clone(), self.utd_hook_manager.get().cloned())
}
pub fn space_service(&self) -> Arc<SpaceService> {
let inner = UISpaceService::new((*self.inner).clone());
pub async fn space_service(&self) -> Arc<SpaceService> {
let inner = UISpaceService::new((*self.inner).clone()).await;
Arc::new(SpaceService::new(inner))
}
@@ -1870,6 +1975,12 @@ impl Client {
.any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_))))
}
/// Checks if the server supports login using a QR code.
pub async fn is_login_with_qr_code_supported(&self) -> Result<bool, ClientError> {
Ok(matches!(self.inner.auth_api(), Some(AuthApi::OAuth(_)))
&& self.inner.unstable_features().await?.contains(&ruma::api::FeatureFlag::Msc4108))
}
/// Get server vendor information from the federation API.
///
/// This method retrieves information about the server's name and version
@@ -2124,11 +2235,12 @@ impl From<&search_users::v3::User> for UserProfile {
impl Client {
fn process_session_change(&self, session_change: SessionChange) {
if let Some(delegate) = self.delegate.get().cloned() {
if let Some(delegate_data) = self.delegate_data.get() {
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 => {}
});
@@ -2224,34 +2336,17 @@ pub struct PowerLevels {
pub events: HashMap<String, i32>,
}
impl From<PowerLevels> for RoomPowerLevelsEventContent {
impl From<PowerLevels> for RoomPowerLevelsContentOverride {
fn from(value: PowerLevels) -> Self {
let mut power_levels = RoomPowerLevelsEventContent::new(&AuthorizationRules::V1);
if let Some(users_default) = value.users_default {
power_levels.users_default = users_default.into();
}
if let Some(state_default) = value.state_default {
power_levels.state_default = state_default.into();
}
if let Some(events_default) = value.events_default {
power_levels.events_default = events_default.into();
}
if let Some(ban) = value.ban {
power_levels.ban = ban.into();
}
if let Some(kick) = value.kick {
power_levels.kick = kick.into();
}
if let Some(redact) = value.redact {
power_levels.redact = redact.into();
}
if let Some(invite) = value.invite {
power_levels.invite = invite.into();
}
if let Some(notifications) = value.notifications {
power_levels.notifications = notifications.into()
}
let mut power_levels = RoomPowerLevelsContentOverride::default();
power_levels.users_default = value.users_default.map(Into::into);
power_levels.state_default = value.state_default.map(Into::into);
power_levels.events_default = value.events_default.map(Into::into);
power_levels.ban = value.ban.map(Into::into);
power_levels.kick = value.kick.map(Into::into);
power_levels.redact = value.redact.map(Into::into);
power_levels.invite = value.invite.map(Into::into);
power_levels.notifications = value.notifications.map(Into::into).unwrap_or_default();
power_levels.users = value
.users
.iter()
@@ -2263,7 +2358,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
}
})
.collect();
power_levels.events = value
.events
.iter()
@@ -2272,7 +2366,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
(event_type, (*power_level).into())
})
.collect();
power_levels
}
}
@@ -2299,6 +2392,8 @@ pub struct CreateRoomParameters {
pub history_visibility_override: Option<RoomHistoryVisibility>,
#[uniffi(default = None)]
pub canonical_alias: Option<String>,
#[uniffi(default = false)]
pub is_space: bool,
}
impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
@@ -2353,8 +2448,14 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
request.initial_state = initial_state;
if value.is_space {
let mut creation_content = CreationContent::new();
creation_content.room_type = Some(RoomType::Space);
request.creation_content = Some(Raw::new(&creation_content)?);
}
if let Some(power_levels) = value.power_level_content_override {
match Raw::new(&power_levels.into()) {
match Raw::<RoomPowerLevelsContentOverride>::new(&power_levels.into()) {
Ok(power_levels) => {
request.power_level_content_override = Some(power_levels);
}
@@ -2559,23 +2660,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,
@@ -2887,3 +2988,66 @@ impl From<matrix_sdk::StoreSizes> for StoreSizes {
}
}
}
#[cfg(test)]
mod tests {
use ruma::{
api::client::room::{create_room, Visibility},
events::StateEventType,
room::RoomType,
};
use crate::{
client::{CreateRoomParameters, JoinRule, RoomPreset, RoomVisibility},
room::RoomHistoryVisibility,
};
#[test]
fn test_create_room_parameters_mapping() {
let params = CreateRoomParameters {
name: Some("A room".to_owned()),
topic: Some("A topic".to_owned()),
is_encrypted: true,
is_direct: true,
visibility: RoomVisibility::Public,
preset: RoomPreset::PublicChat,
invite: Some(vec!["@user:example.com".to_owned()]),
avatar: Some("http://example.com/avatar.jpg".to_owned()),
power_level_content_override: None,
join_rule_override: Some(JoinRule::Knock),
history_visibility_override: Some(RoomHistoryVisibility::Shared),
canonical_alias: Some("#a-room:example.com".to_owned()),
is_space: true,
};
let request: create_room::v3::Request =
params.try_into().expect("CreateRoomParameters couldn't be transformed into a Request");
let initial_state = request
.initial_state
.iter()
.map(|raw| raw.deserialize().expect("Initial state event failed to deserialize"))
.collect::<Vec<_>>();
assert_eq!(request.name, Some("A room".to_owned()));
assert_eq!(request.topic, Some("A topic".to_owned()));
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomEncryption));
assert!(request.is_direct);
assert_eq!(request.visibility, Visibility::Public);
assert_eq!(request.preset, Some(create_room::v3::RoomPreset::PublicChat));
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_eq!(request.room_alias_name, Some("#a-room:example.com".to_owned()));
let room_type = request
.creation_content
.expect("Creation content is missing")
.deserialize()
.expect("Creation content can't be deserialized")
.room_type;
assert_eq!(room_type, Some(RoomType::Space));
}
}
+73 -47
View File
@@ -1,11 +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.
// Allow UniFFI to use methods marked as `#[deprecated]`.
#![allow(deprecated)]
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
#[cfg(not(target_family = "wasm"))]
#[cfg(not(target_os = "android"))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
@@ -115,8 +130,7 @@ 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,
@@ -160,8 +174,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(),
@@ -183,18 +196,12 @@ impl ClientBuilder {
})
}
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)
}
@@ -352,12 +359,8 @@ impl ClientBuilder {
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()? {
@@ -406,27 +409,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();
@@ -498,15 +508,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 +626,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,
}
}
}
+33 -5
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;
use futures_util::StreamExt;
@@ -398,6 +412,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 +421,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()
}
@@ -563,11 +595,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) {
+36 -16
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, error::Error, fmt, fmt::Display};
use matrix_sdk::{
@@ -736,7 +750,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 +764,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 +778,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 +806,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 +825,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 +836,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() })
+277 -15
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 anyhow::{bail, Context};
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
@@ -11,13 +25,14 @@ use ruma::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
TimelineEventType as RumaTimelineEventType,
},
EventId,
};
use crate::{
room_member::MembershipState,
ruma::{MessageType, RtcNotificationType},
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
utils::Timestamp,
ClientError,
};
@@ -39,16 +54,16 @@ impl TimelineEvent {
self.0.origin_server_ts().into()
}
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
let event_type = match &*self.0 {
pub fn content(&self) -> Result<TimelineEventContent, ClientError> {
let content = match &*self.0 {
AnySyncTimelineEvent::MessageLike(event) => {
TimelineEventType::MessageLike { content: event.clone().try_into()? }
TimelineEventContent::MessageLike { content: event.clone().try_into()? }
}
AnySyncTimelineEvent::State(event) => {
TimelineEventType::State { content: event.clone().try_into()? }
TimelineEventContent::State { content: event.clone().try_into()? }
}
};
Ok(event_type)
Ok(content)
}
/// Returns the thread root event id for the event, if it's part of a
@@ -72,16 +87,216 @@ impl From<AnyTimelineEvent> for TimelineEvent {
}
}
/// The timeline event type.
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
pub enum TimelineEventType {
/// The event is a message-like one and should be displayed as such.
MessageLike { value: MessageLikeEventType },
/// The event is a state event, and may or may not be displayed in the
/// timeline.
State { value: StateEventType },
}
impl From<RumaTimelineEventType> for TimelineEventType {
fn from(value: RumaTimelineEventType) -> Self {
match value {
RumaTimelineEventType::Audio => {
Self::MessageLike { value: MessageLikeEventType::Audio }
}
RumaTimelineEventType::File => Self::MessageLike { value: MessageLikeEventType::File },
RumaTimelineEventType::Image => {
Self::MessageLike { value: MessageLikeEventType::Image }
}
RumaTimelineEventType::Video => {
Self::MessageLike { value: MessageLikeEventType::Video }
}
RumaTimelineEventType::Voice => {
Self::MessageLike { value: MessageLikeEventType::Voice }
}
RumaTimelineEventType::Emote => {
Self::MessageLike { value: MessageLikeEventType::Emote }
}
RumaTimelineEventType::Encrypted => {
Self::MessageLike { value: MessageLikeEventType::Encrypted }
}
RumaTimelineEventType::RoomMessage => {
Self::MessageLike { value: MessageLikeEventType::RoomMessage }
}
RumaTimelineEventType::CallAnswer => {
Self::MessageLike { value: MessageLikeEventType::CallAnswer }
}
RumaTimelineEventType::CallInvite => {
Self::MessageLike { value: MessageLikeEventType::CallInvite }
}
RumaTimelineEventType::CallHangup => {
Self::MessageLike { value: MessageLikeEventType::CallHangup }
}
RumaTimelineEventType::CallCandidates => {
Self::MessageLike { value: MessageLikeEventType::CallCandidates }
}
RumaTimelineEventType::CallNegotiate => {
Self::MessageLike { value: MessageLikeEventType::CallNegotiate }
}
RumaTimelineEventType::CallReject => {
Self::MessageLike { value: MessageLikeEventType::CallReject }
}
RumaTimelineEventType::CallSdpStreamMetadataChanged => {
Self::MessageLike { value: MessageLikeEventType::CallSdpStreamMetadataChanged }
}
RumaTimelineEventType::CallSelectAnswer => {
Self::MessageLike { value: MessageLikeEventType::CallSelectAnswer }
}
RumaTimelineEventType::KeyVerificationReady => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationReady }
}
RumaTimelineEventType::KeyVerificationStart => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationStart }
}
RumaTimelineEventType::KeyVerificationCancel => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationCancel }
}
RumaTimelineEventType::KeyVerificationAccept => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationAccept }
}
RumaTimelineEventType::KeyVerificationKey => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationKey }
}
RumaTimelineEventType::KeyVerificationMac => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationMac }
}
RumaTimelineEventType::KeyVerificationDone => {
Self::MessageLike { value: MessageLikeEventType::KeyVerificationDone }
}
RumaTimelineEventType::Location => {
Self::MessageLike { value: MessageLikeEventType::Location }
}
RumaTimelineEventType::Message => {
Self::MessageLike { value: MessageLikeEventType::Message }
}
RumaTimelineEventType::PollStart => {
Self::MessageLike { value: MessageLikeEventType::PollStart }
}
RumaTimelineEventType::UnstablePollStart => {
Self::MessageLike { value: MessageLikeEventType::UnstablePollStart }
}
RumaTimelineEventType::PollResponse => {
Self::MessageLike { value: MessageLikeEventType::PollResponse }
}
RumaTimelineEventType::UnstablePollResponse => {
Self::MessageLike { value: MessageLikeEventType::UnstablePollResponse }
}
RumaTimelineEventType::PollEnd => {
Self::MessageLike { value: MessageLikeEventType::PollEnd }
}
RumaTimelineEventType::UnstablePollEnd => {
Self::MessageLike { value: MessageLikeEventType::UnstablePollEnd }
}
RumaTimelineEventType::Beacon => {
Self::MessageLike { value: MessageLikeEventType::Beacon }
}
RumaTimelineEventType::Reaction => {
Self::MessageLike { value: MessageLikeEventType::Reaction }
}
RumaTimelineEventType::RoomEncrypted => {
Self::MessageLike { value: MessageLikeEventType::RoomEncrypted }
}
RumaTimelineEventType::RoomRedaction => {
Self::MessageLike { value: MessageLikeEventType::RoomRedaction }
}
RumaTimelineEventType::Sticker => {
Self::MessageLike { value: MessageLikeEventType::Sticker }
}
RumaTimelineEventType::CallNotify => {
Self::MessageLike { value: MessageLikeEventType::CallNotify }
}
RumaTimelineEventType::RtcNotification => {
Self::MessageLike { value: MessageLikeEventType::RtcNotification }
}
RumaTimelineEventType::RtcDecline => {
Self::MessageLike { value: MessageLikeEventType::RtcDecline }
}
RumaTimelineEventType::PolicyRuleRoom => {
Self::State { value: StateEventType::PolicyRuleRoom }
}
RumaTimelineEventType::PolicyRuleServer => {
Self::State { value: StateEventType::PolicyRuleServer }
}
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 }
}
RumaTimelineEventType::RoomCreate => Self::State { value: StateEventType::RoomCreate },
RumaTimelineEventType::RoomEncryption => {
Self::State { value: StateEventType::RoomEncryption }
}
RumaTimelineEventType::RoomGuestAccess => {
Self::State { value: StateEventType::RoomGuestAccess }
}
RumaTimelineEventType::RoomHistoryVisibility => {
Self::State { value: StateEventType::RoomHistoryVisibility }
}
RumaTimelineEventType::RoomJoinRules => {
Self::State { value: StateEventType::RoomJoinRules }
}
RumaTimelineEventType::RoomMember => {
Self::State { value: StateEventType::RoomMemberEvent }
}
RumaTimelineEventType::RoomLanguage => {
Self::State { value: StateEventType::RoomLanguage }
}
RumaTimelineEventType::RoomName => Self::State { value: StateEventType::RoomName },
RumaTimelineEventType::RoomImagePack => {
Self::State { value: StateEventType::RoomImagePack }
}
RumaTimelineEventType::RoomPinnedEvents => {
Self::State { value: StateEventType::RoomPinnedEvents }
}
RumaTimelineEventType::RoomPowerLevels => {
Self::State { value: StateEventType::RoomPowerLevels }
}
RumaTimelineEventType::RoomServerAcl => {
Self::State { value: StateEventType::RoomServerAcl }
}
RumaTimelineEventType::RoomThirdPartyInvite => {
Self::State { value: StateEventType::RoomThirdPartyInvite }
}
RumaTimelineEventType::RoomTombstone => {
Self::State { value: StateEventType::RoomTombstone }
}
RumaTimelineEventType::RoomTopic => Self::State { value: StateEventType::RoomTopic },
RumaTimelineEventType::SpaceChild => Self::State { value: StateEventType::SpaceChild },
RumaTimelineEventType::SpaceParent => {
Self::State { value: StateEventType::SpaceParent }
}
RumaTimelineEventType::BeaconInfo => Self::State { value: StateEventType::BeaconInfo },
RumaTimelineEventType::CallMember => Self::State { value: StateEventType::CallMember },
RumaTimelineEventType::MemberHints => {
Self::State { value: StateEventType::MemberHints }
}
RumaTimelineEventType::_Custom(_) => {
Self::State { value: StateEventType::Custom { value: value.to_string() } }
}
_ => Self::MessageLike { value: MessageLikeEventType::Other(value.to_string()) },
}
}
}
#[derive(uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `TimelineEventType`, we would need to
// In order to reduce the size of `TimelineEventContent`, we would need to
// put some parts in a `Box`, or an `Arc`. Sadly, it doesn't play well with
// UniFFI. We would need to change the `uniffi::Record` of the subtypes into
// `uniffi::Object`, which is a radical change. It would simplify the memory
// usage, but it would slow down the performance around the FFI border. Thus,
// let's consider this is a false-positive lint in this particular case.
#[allow(clippy::large_enum_variant)]
pub enum TimelineEventType {
pub enum TimelineEventContent {
MessageLike { content: MessageLikeEventContent },
State { content: StateEventContent },
}
@@ -170,6 +385,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,
@@ -212,6 +429,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,
@@ -254,7 +472,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 {
@@ -300,9 +518,11 @@ where
Ok(original_content)
}
#[derive(Clone, uniffi::Enum)]
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
pub enum StateEventType {
BeaconInfo,
CallMember,
MemberHints,
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
@@ -313,8 +533,10 @@ pub enum StateEventType {
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility,
RoomImagePack,
RoomJoinRules,
RoomMemberEvent,
RoomLanguage,
RoomName,
RoomPinnedEvents,
RoomPowerLevels,
@@ -324,12 +546,15 @@ pub enum StateEventType {
RoomTopic,
SpaceChild,
SpaceParent,
Custom { value: String },
}
impl From<StateEventType> for ruma::events::StateEventType {
fn from(val: StateEventType) -> Self {
match val {
StateEventType::BeaconInfo => Self::BeaconInfo,
StateEventType::CallMember => Self::CallMember,
StateEventType::MemberHints => Self::MemberHints,
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
@@ -340,7 +565,9 @@ impl From<StateEventType> for ruma::events::StateEventType {
StateEventType::RoomEncryption => Self::RoomEncryption,
StateEventType::RoomGuestAccess => Self::RoomGuestAccess,
StateEventType::RoomHistoryVisibility => Self::RoomHistoryVisibility,
StateEventType::RoomImagePack => Self::RoomImagePack,
StateEventType::RoomJoinRules => Self::RoomJoinRules,
StateEventType::RoomLanguage => Self::RoomLanguage,
StateEventType::RoomMemberEvent => Self::RoomMember,
StateEventType::RoomName => Self::RoomName,
StateEventType::RoomPinnedEvents => Self::RoomPinnedEvents,
@@ -351,17 +578,28 @@ impl From<StateEventType> for ruma::events::StateEventType {
StateEventType::RoomTopic => Self::RoomTopic,
StateEventType::SpaceChild => Self::SpaceChild,
StateEventType::SpaceParent => Self::SpaceParent,
StateEventType::Custom { value } => value.into(),
}
}
}
#[derive(Clone, uniffi::Enum)]
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
pub enum MessageLikeEventType {
Audio,
Beacon,
CallAnswer,
CallCandidates,
CallHangup,
CallInvite,
RtcNotification,
CallNegotiate,
CallNotify,
CallReject,
CallSdpStreamMetadataChanged,
CallSelectAnswer,
Emote,
Encrypted,
File,
Image,
KeyVerificationAccept,
KeyVerificationCancel,
KeyVerificationDone,
@@ -369,6 +607,8 @@ pub enum MessageLikeEventType {
KeyVerificationMac,
KeyVerificationReady,
KeyVerificationStart,
Location,
Message,
PollEnd,
PollResponse,
PollStart,
@@ -376,21 +616,39 @@ pub enum MessageLikeEventType {
RoomEncrypted,
RoomMessage,
RoomRedaction,
RtcDecline,
RtcNotification,
Sticker,
UnstablePollEnd,
UnstablePollResponse,
UnstablePollStart,
Video,
Voice,
Other(String),
}
impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
fn from(val: MessageLikeEventType) -> Self {
match val {
MessageLikeEventType::Audio => Self::Audio,
MessageLikeEventType::File => Self::File,
MessageLikeEventType::Image => Self::Image,
MessageLikeEventType::Video => Self::Video,
MessageLikeEventType::Voice => Self::Voice,
MessageLikeEventType::Beacon => Self::Beacon,
MessageLikeEventType::CallAnswer => Self::CallAnswer,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::RtcNotification => Self::RtcNotification,
MessageLikeEventType::CallHangup => Self::CallHangup,
MessageLikeEventType::CallCandidates => Self::CallCandidates,
MessageLikeEventType::CallInvite => Self::CallInvite,
MessageLikeEventType::CallHangup => Self::CallHangup,
MessageLikeEventType::CallNegotiate => Self::CallNegotiate,
MessageLikeEventType::CallNotify => Self::CallNotify,
MessageLikeEventType::CallReject => Self::CallReject,
MessageLikeEventType::CallSdpStreamMetadataChanged => {
Self::CallSdpStreamMetadataChanged
}
MessageLikeEventType::CallSelectAnswer => Self::CallSelectAnswer,
MessageLikeEventType::Emote => Self::Emote,
MessageLikeEventType::Encrypted => Self::Encrypted,
MessageLikeEventType::KeyVerificationReady => Self::KeyVerificationReady,
MessageLikeEventType::KeyVerificationStart => Self::KeyVerificationStart,
MessageLikeEventType::KeyVerificationCancel => Self::KeyVerificationCancel,
@@ -398,14 +656,18 @@ impl From<MessageLikeEventType> for ruma::events::MessageLikeEventType {
MessageLikeEventType::KeyVerificationKey => Self::KeyVerificationKey,
MessageLikeEventType::KeyVerificationMac => Self::KeyVerificationMac,
MessageLikeEventType::KeyVerificationDone => Self::KeyVerificationDone,
MessageLikeEventType::Location => Self::Location,
MessageLikeEventType::Message => Self::Message,
MessageLikeEventType::Reaction => Self::Reaction,
MessageLikeEventType::RoomEncrypted => Self::RoomEncrypted,
MessageLikeEventType::RoomMessage => Self::RoomMessage,
MessageLikeEventType::RoomRedaction => Self::RoomRedaction,
MessageLikeEventType::RtcDecline => Self::RtcDecline,
MessageLikeEventType::Sticker => Self::Sticker,
MessageLikeEventType::PollEnd => Self::PollEnd,
MessageLikeEventType::PollResponse => Self::PollResponse,
MessageLikeEventType::PollStart => Self::PollStart,
MessageLikeEventType::RtcNotification => Self::RtcNotification,
MessageLikeEventType::UnstablePollEnd => Self::UnstablePollEnd,
MessageLikeEventType::UnstablePollResponse => Self::UnstablePollResponse,
MessageLikeEventType::UnstablePollStart => Self::UnstablePollStart,
+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 {
-1
View File
@@ -30,7 +30,6 @@ mod store;
mod sync_service;
mod task_handle;
mod timeline;
mod tracing;
mod utd;
mod utils;
mod widget;
@@ -9,8 +9,9 @@
// 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;
#[derive(uniffi::Record)]
pub struct LastLocation {
@@ -1,8 +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, sync::Arc};
use matrix_sdk_ui::notification_client::{
NotificationClient as SdkNotificationClient, NotificationEvent as SdkNotificationEvent,
NotificationItem as SdkNotificationItem, NotificationStatus as SdkNotificationStatus,
RawNotificationEvent as SdkRawNotificationEvent,
};
use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId};
@@ -43,6 +58,9 @@ pub struct NotificationRoomInfo {
pub struct NotificationItem {
pub event: NotificationEvent,
/// The raw JSON of the underlying event.
pub raw_event: String,
pub sender_info: NotificationSenderInfo,
pub room_info: NotificationRoomInfo,
@@ -67,8 +85,15 @@ impl NotificationItem {
NotificationEvent::Invite { sender: event.sender.to_string() }
}
};
let raw_event = match &item.raw_event {
SdkRawNotificationEvent::Timeline(raw) => raw.json().get().to_owned(),
SdkRawNotificationEvent::Invite(raw) => raw.json().get().to_owned(),
};
Self {
event,
raw_event,
sender_info: NotificationSenderInfo {
display_name: item.sender_display_name,
avatar_url: item.sender_avatar_url,
@@ -106,6 +131,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 {
@@ -116,6 +143,7 @@ impl From<SdkNotificationStatus> for NotificationStatus {
}
SdkNotificationStatus::EventNotFound => NotificationStatus::EventNotFound,
SdkNotificationStatus::EventFilteredOut => NotificationStatus::EventFilteredOut,
SdkNotificationStatus::EventRedacted => NotificationStatus::EventRedacted,
}
}
}
@@ -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, RwLock};
use matrix_sdk::{
@@ -0,0 +1,89 @@
use std::error::Error;
use jni::{
errors::JniError,
sys::{jint, jsize, JavaVM},
};
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);
}
}
});
}
type JniGetCreatedJavaVms =
unsafe extern "system" fn(_: *mut *mut JavaVM, _: jsize, _: *mut jsize) -> jint;
const JNI_GET_JAVA_VMS_NAME: &[u8] = b"JNI_GetCreatedJavaVMs";
fn get_java_vm() -> Result<jni::JavaVM, Box<dyn Error>> {
// Use libloading to avoid having to create and expose a JNI function and call
// it from the Android side.
let library = libloading::os::unix::Library::this();
let get_created_java_vms: JniGetCreatedJavaVms =
unsafe { *library.get(JNI_GET_JAVA_VMS_NAME)? };
let mut java_vms: [*mut JavaVM; 1] = [std::ptr::null_mut() as *mut JavaVM];
let mut vm_count: i32 = 0;
let ok = unsafe { get_created_java_vms(java_vms.as_mut_ptr(), 1, &mut vm_count) };
if ok != jni::sys::JNI_OK {
return Err("Failed to get JavaVM".into());
}
if vm_count != 1 {
return Err(format!("Invalid JavaVM count: {vm_count}").into());
}
let jvm = unsafe { jni::JavaVM::from_raw(java_vms[0]) }?;
Ok(jvm)
}
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,10 +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.
use std::sync::OnceLock;
#[cfg(feature = "sentry")]
use std::sync::{atomic::AtomicBool, Arc};
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;
@@ -23,9 +38,25 @@ use tracing_subscriber::{
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;
use self::tracing::LogLevel;
#[cfg(feature = "sentry")]
use crate::tracing::BRIDGE_SPAN_NAME;
use crate::{error::ClientError, tracing::LogLevel};
use self::tracing::BRIDGE_SPAN_NAME;
// Adjusted version of tracing_subscriber::fmt::Format
struct EventFormatter {
@@ -153,7 +184,7 @@ type ReloadHandle = Handle<
Layered<EnvFilter, Registry>,
FieldsFormatterForFiles,
EventFormatter,
RollingFileAppender,
SizeAndDateRollingWriter,
>,
Layered<EnvFilter, Registry>,
>;
@@ -218,21 +249,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 +281,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)]
@@ -289,6 +333,7 @@ enum LogTarget {
MatrixSdkEventCache,
MatrixSdkEventCacheStore,
MatrixSdkHttpClient,
MatrixSdkLatestEvents,
MatrixSdkOidc,
MatrixSdkSendQueue,
MatrixSdkSlidingSync,
@@ -319,6 +364,7 @@ impl LogTarget {
LogTarget::MatrixSdkHttpClient => "matrix_sdk::http_client",
LogTarget::MatrixSdkSlidingSync => "matrix_sdk::sliding_sync",
LogTarget::MatrixSdkEventCache => "matrix_sdk::event_cache",
LogTarget::MatrixSdkLatestEvents => "matrix_sdk::latest_events",
LogTarget::MatrixSdkSendQueue => "matrix_sdk::send_queue",
LogTarget::MatrixSdkEventCacheStore => "matrix_sdk_sqlite::event_cache_store",
LogTarget::MatrixSdkUiTimeline => "matrix_sdk_ui::timeline",
@@ -341,6 +387,7 @@ const DEFAULT_TARGET_LOG_LEVELS: &[(LogTarget, LogLevel)] = &[
(LogTarget::MatrixSdkUiTimeline, LogLevel::Info),
(LogTarget::MatrixSdkSendQueue, LogLevel::Info),
(LogTarget::MatrixSdkEventCache, LogLevel::Info),
(LogTarget::MatrixSdkLatestEvents, LogLevel::Info),
(LogTarget::MatrixSdkBaseEventCache, LogLevel::Info),
(LogTarget::MatrixSdkEventCacheStore, LogLevel::Info),
(LogTarget::MatrixSdkCommonCrossProcessLock, LogLevel::Warn),
@@ -372,6 +419,8 @@ pub enum TraceLogPacks {
NotificationClient,
/// Enables all the logs relevant to sync profiling.
SyncProfiling,
/// Enables all the logs relevant to the latest events.
LatestEvents,
}
impl TraceLogPacks {
@@ -399,6 +448,11 @@ impl TraceLogPacks {
LogTarget::MatrixSdkCommonCrossProcessLock,
LogTarget::MatrixSdkCommonDeserializedResponses,
],
TraceLogPacks::LatestEvents => &[
LogTarget::MatrixSdkLatestEvents,
LogTarget::MatrixSdkSendQueue,
LogTarget::MatrixSdkEventCache,
],
}
}
}
@@ -441,9 +495,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 {
@@ -464,10 +526,10 @@ 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
@@ -483,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.
@@ -548,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
}
@@ -618,6 +685,9 @@ pub fn init_platform(
}
}
#[cfg(target_os = "android")]
android_platform::init();
Ok(())
}
@@ -675,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
}));
}
@@ -725,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);
@@ -746,6 +820,7 @@ mod tests {
matrix_sdk_ui::timeline=info,
matrix_sdk::send_queue=info,
matrix_sdk::event_cache=info,
matrix_sdk::latest_events=info,
matrix_sdk_base::event_cache=info,
matrix_sdk_sqlite::event_cache_store=info,
matrix_sdk_common::cross_process_lock=warn,
@@ -770,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);
@@ -791,6 +866,7 @@ mod tests {
matrix_sdk_ui::timeline=trace,
matrix_sdk::send_queue=trace,
matrix_sdk::event_cache=trace,
matrix_sdk::latest_events=trace,
matrix_sdk_base::event_cache=trace,
matrix_sdk_sqlite::event_cache_store=trace,
matrix_sdk_common::cross_process_lock=warn,
@@ -816,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);
@@ -837,6 +913,7 @@ mod tests {
matrix_sdk_ui::timeline=info,
matrix_sdk::send_queue=trace,
matrix_sdk::event_cache=trace,
matrix_sdk::latest_events=info,
matrix_sdk_base::event_cache=trace,
matrix_sdk_sqlite::event_cache_store=trace,
matrix_sdk_common::cross_process_lock=warn,
@@ -0,0 +1,923 @@
// 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 {
if let Some(state) = state.as_ref() {
if !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) {
if 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 {
if 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,11 +1,24 @@
// 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_core::{identify_callsite, metadata::Kind as MetadataKind};
@@ -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) {
+103 -10
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;
use matrix_sdk::authentication::oauth::{
@@ -7,6 +21,7 @@ use matrix_sdk::authentication::oauth::{
},
OAuth,
};
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use crate::{
@@ -236,17 +251,48 @@ impl QrCodeData {
Ok(Self { inner: qrcode::QrCodeData::from_bytes(&bytes)? }.into())
}
/// Serialize the [`QrCodeData`] into a byte vector for encoding as a QR
/// code.
pub fn to_bytes(&self) -> Vec<u8> {
self.inner.to_bytes()
}
/// The server name contained within the scanned QR code data.
///
/// Note: This value is only present when scanning a QR code the belongs to
/// Note: This value is only present when scanning a QR code that belongs to
/// a logged in client. The mode where the new client shows the QR code
/// will return `None`.
pub fn server_name(&self) -> Option<String> {
match &self.inner.mode_data {
qrcode::QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
qrcode::QrCodeModeData::Login => None,
match &self.inner.intent_data() {
qr_login::QrCodeIntentData::Msc4108 { data, .. } => match data {
qrcode::Msc4108IntentData::Login => None,
qrcode::Msc4108IntentData::Reciprocate { server_name } => {
Some(server_name.to_owned())
}
},
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`].
@@ -286,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 {
@@ -317,6 +365,9 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::UnsupportedQrCodeType => {
HumanQrLoginError::UnsupportedQrCodeType
}
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode
@@ -371,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())
}
@@ -395,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()),
},
}
}
}
+45 -30
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, fs, path::PathBuf, pin::pin, sync::Arc};
use anyhow::{Context, Result};
@@ -6,7 +20,6 @@ use matrix_sdk::{
encryption::LocalTrust,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
TryFromReportedContentScoreError,
},
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
@@ -53,8 +66,8 @@ use crate::{
runtime::get_runtime_handle,
timeline::{
configuration::{TimelineConfiguration, TimelineFilter},
AbstractProgress, EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
UploadSource,
threads::{ListThreadsOptions, ThreadList, ThreadSubscription},
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
},
utils::{u64_to_uint, AsyncRuntimeDropped},
TaskHandle,
@@ -258,10 +271,10 @@ impl Room {
});
}
TimelineFilter::EventTypeFilter { filter: event_type_filter } => {
TimelineFilter::EventFilter { filter: event_filter } => {
builder = builder.event_filter(move |event, room_version_id| {
// Always perform the default filter first
default_event_filter(event, room_version_id) && event_type_filter.filter(event)
default_event_filter(event, room_version_id) && event_filter.filter(event)
});
}
}
@@ -303,12 +316,8 @@ impl Room {
.unwrap_or(false)
}
async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.latest_event_item().await.map(Into::into)
}
async fn new_latest_event(&self) -> LatestEventValue {
self.inner.new_latest_event().await.into()
async fn latest_event(&self) -> LatestEventValue {
self.inner.latest_event().await.into()
}
pub async fn latest_encryption_state(&self) -> Result<EncryptionState, ClientError> {
@@ -348,6 +357,14 @@ impl Room {
Ok(avatar_url_string)
}
pub async fn set_own_member_display_name(
&self,
display_name: Option<String>,
) -> Result<(), ClientError> {
self.inner.set_own_member_display_name(display_name).await?;
Ok(())
}
/// Get the membership details for the current user.
///
/// Returns:
@@ -465,18 +482,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(())
}
@@ -1123,7 +1131,7 @@ impl Room {
geo_uri: event.last_location.location.uri.clone().to_string(),
description: None,
zoom_level: None,
asset: None,
asset: event.beacon_info.as_ref().map(|b| b.asset.type_.clone()).into(),
};
let Some(beacon_info) = event.beacon_info else {
@@ -1233,6 +1241,21 @@ impl Room {
.map(|sub| ThreadSubscription { automatic: sub.automatic }))
}
/// Retrieve a list of all the threads for the current 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
/// [`ThreadList::prev_batch_token`]. This token can be passed to the next
/// call to this function, through the `from` field of
/// [`ListThreadsOptions`].
pub async fn load_thread_list(
&self,
opts: ListThreadsOptions,
) -> Result<ThreadList, ClientError> {
let thread_list = self.inner.load_thread_list(opts.into()).await?;
Ok(thread_list.into())
}
/// Either loads the event associated with the `event_id` from the event
/// cache or fetches it from the homeserver.
pub async fn load_or_fetch_event(
@@ -1250,14 +1273,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,
}
/// A listener for receiving new live location shares in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
@@ -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;
use anyhow::Result;
@@ -29,6 +43,10 @@ impl RoomPowerLevels {
self.inner.clone().into()
}
fn events(&self) -> HashMap<crate::event::TimelineEventType, i64> {
self.inner.events.iter().map(|(key, value)| (key.clone().into(), (*value).into())).collect()
}
/// Gets a map with the `UserId` of users with power levels other than `0`
/// and their power level.
pub fn user_power_levels(&self) -> HashMap<String, i64> {
+50 -1
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::{
@@ -11,8 +25,32 @@ use crate::{
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
},
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};
+26 -18
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.
#![allow(deprecated)]
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
@@ -15,10 +29,10 @@ 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_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,
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,
},
unable_to_decrypt_hook::UtdHookManager,
};
@@ -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.
@@ -472,6 +474,7 @@ impl RoomListDynamicEntriesController {
pub enum RoomListEntriesDynamicFilterKind {
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
Identifiers { identifiers: Vec<String> },
NonSpace,
Space,
NonLeft,
@@ -482,6 +485,7 @@ pub enum RoomListEntriesDynamicFilterKind {
Favourite,
LowPriority,
NonLowPriority,
NonFavorite,
Invite,
Category { expect: RoomListFilterCategory },
None,
@@ -516,6 +520,9 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
Kind::Any { filters } => Box::new(new_filter_any(
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
)),
Kind::Identifiers { identifiers } => Box::new(new_filter_identifiers(
identifiers.into_iter().map(|id| RoomId::parse(id).unwrap()).collect(),
)),
Kind::NonSpace => Box::new(new_filter_not(Box::new(new_filter_space()))),
Kind::Space => Box::new(new_filter_space()),
Kind::NonLeft => Box::new(new_filter_non_left()),
@@ -524,6 +531,7 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
Kind::Favourite => Box::new(new_filter_favourite()),
Kind::LowPriority => Box::new(new_filter_low_priority()),
Kind::NonLowPriority => Box::new(new_filter_not(Box::new(new_filter_low_priority()))),
Kind::NonFavorite => Box::new(new_filter_not(Box::new(new_filter_favourite()))),
Kind::Invite => Box::new(new_filter_invite()),
Kind::Category { expect } => Box::new(new_filter_category(expect.into())),
Kind::None => Box::new(new_filter_none()),
@@ -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 anyhow::Context as _;
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
+55 -11
View File
@@ -56,7 +56,9 @@ use ruma::{
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::{
@@ -470,11 +472,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 +508,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 +566,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 +605,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 +642,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 +677,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 +923,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 +938,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(),
}
}
}
@@ -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, RwLock};
use futures_util::StreamExt;
@@ -264,7 +278,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(),
});
}
+136 -12
View File
@@ -20,7 +20,8 @@ use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::spaces::{
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
room_list::SpaceRoomListPaginationState,
SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
SpaceService as UISpaceService,
};
use ruma::RoomId;
@@ -55,17 +56,17 @@ impl SpaceService {
/// Returns a list of all the top-level joined spaces. It will eagerly
/// compute the latest version and also notify subscribers if there were
/// any changes.
pub async fn joined_spaces(&self) -> Vec<SpaceRoom> {
self.inner.joined_spaces().await.into_iter().map(Into::into).collect()
pub async fn top_level_joined_spaces(&self) -> Vec<SpaceRoom> {
self.inner.top_level_joined_spaces().await.into_iter().map(Into::into).collect()
}
/// Subscribes to updates on the joined spaces list. If space rooms are
/// joined or left, the stream will yield diffs that reflect the changes.
pub async fn subscribe_to_joined_spaces(
pub async fn subscribe_to_top_level_joined_spaces(
&self,
listener: Box<dyn SpaceServiceJoinedSpacesListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_joined_spaces().await;
let (initial_values, mut stream) = self.inner.subscribe_to_top_level_joined_spaces().await;
listener.on_update(vec![SpaceListUpdate::Reset {
values: initial_values.into_iter().map(Into::into).collect(),
@@ -78,11 +79,40 @@ impl SpaceService {
})))
}
/// Space filters provide access to a custom subset of the space graph that
/// can be used in tandem with the [`crate::RoomListService`] to narrow
/// down the presented rooms.
///
/// They are limited to the first 2 levels of the graph, with the first
/// level only containing direct descendants while the second holds the rest
/// of them recursively.
pub async fn space_filters(&self) -> Vec<SpaceFilter> {
self.inner.space_filters().await.into_iter().map(|s| s.into()).collect()
}
/// Subscribe to changes or updates to the space filters.
pub async fn subscribe_to_space_filters(
&self,
listener: Box<dyn SpaceServiceSpaceFiltersListener>,
) -> Arc<TaskHandle> {
let (initial_values, mut stream) = self.inner.subscribe_to_space_filters().await;
listener.on_update(vec![SpaceFilterUpdate::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());
}
})))
}
/// Returns a flattened list containing all the spaces where the user has
/// permission to send `m.space.child` state events.
///
/// Note: Unlike [`Self::joined_spaces()`], this method does not recompute
/// the space graph, nor does it notify subscribers about changes.
/// Note: Unlike [`Self::top_level_joined_spaces()`], this method does not
/// recompute the space graph, nor does it notify subscribers about changes.
pub async fn editable_spaces(&self) -> Vec<SpaceRoom> {
self.inner.editable_spaces().await.into_iter().map(Into::into).collect()
}
@@ -108,6 +138,13 @@ impl SpaceService {
Ok(parents.into_iter().map(Into::into).collect())
}
/// Returns the corresponding `SpaceRoom` for the given room ID, or `None`
/// if it isn't known.
pub async fn get_space_room(&self, room_id: String) -> Result<Option<SpaceRoom>, ClientError> {
let room_id = RoomId::parse(room_id.as_str())?;
Ok(self.inner.get_space_room(&room_id).await.map(Into::into))
}
pub async fn add_child_to_space(
&self,
child_id: String,
@@ -149,7 +186,7 @@ impl SpaceService {
}
}
/// The `SpaceRoomList`represents a paginated list of direct rooms
/// The `SpaceRoomList` represents a paginated list of direct rooms
/// that belong to a particular space.
///
/// It can be used to paginate through the list (and have live updates on the
@@ -242,6 +279,18 @@ impl SpaceRoomList {
pub async fn paginate(&self) -> Result<(), ClientError> {
self.inner.paginate().await.map_err(ClientError::from)
}
/// Clears the room list back to its initial state so that any new changes
/// to the hierarchy will be included the next time [`Self::paginate`] is
/// called.
///
/// This is useful when you've added or removed children from the space as
/// the list is based on a cached state that lives server-side, meaning
/// the /hierarchy request needs to be restarted from scratch to pick up
/// the changes.
pub async fn reset(&self) {
self.inner.reset().await;
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
@@ -264,6 +313,11 @@ pub trait SpaceServiceJoinedSpacesListener: SendOutsideWasm + SyncOutsideWasm +
fn on_update(&self, room_updates: Vec<SpaceListUpdate>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SpaceServiceSpaceFiltersListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, filter_updates: Vec<SpaceFilterUpdate>);
}
/// Structure representing a room in a space and aggregated information
/// relevant to the UI layer.
#[derive(uniffi::Record)]
@@ -371,6 +425,47 @@ impl From<VectorDiff<UISpaceRoom>> for SpaceListUpdate {
}
}
#[derive(uniffi::Enum)]
pub enum SpaceFilterUpdate {
Append { values: Vec<SpaceFilter> },
Clear,
PushFront { value: SpaceFilter },
PushBack { value: SpaceFilter },
PopFront,
PopBack,
Insert { index: u32, value: SpaceFilter },
Set { index: u32, value: SpaceFilter },
Remove { index: u32 },
Truncate { length: u32 },
Reset { values: Vec<SpaceFilter> },
}
impl From<VectorDiff<UISpaceFilter>> for SpaceFilterUpdate {
fn from(diff: VectorDiff<UISpaceFilter>) -> Self {
match diff {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(|v| v.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(|v| v.into()).collect() }
}
}
}
}
/// The `LeaveSpaceHandle` processes rooms to be left in the order they were
/// provided by the [`SpaceService`] and annotates them with extra data to
/// inform the leave process e.g. if the current user is the last room admin.
@@ -413,14 +508,43 @@ impl From<UILeaveSpaceHandle> for LeaveSpaceHandle {
#[derive(uniffi::Record)]
pub struct LeaveSpaceRoom {
/// The underlying [`SpaceRoom`]
space_room: SpaceRoom,
/// Whether the user is the last admin in the room. This helps clients
pub space_room: SpaceRoom,
/// Whether the user is the last owner in the room. This helps clients
/// better inform the user about the consequences of leaving the room.
is_last_admin: bool,
pub is_last_owner: bool,
/// If the room creators have infinite PL.
pub are_creators_privileged: bool,
}
impl From<UILeaveSpaceRoom> for LeaveSpaceRoom {
fn from(room: UILeaveSpaceRoom) -> Self {
LeaveSpaceRoom { space_room: room.space_room.into(), is_last_admin: room.is_last_admin }
LeaveSpaceRoom {
space_room: room.space_room.into(),
is_last_owner: room.is_last_owner,
are_creators_privileged: room.are_creators_privileged,
}
}
}
#[derive(uniffi::Record)]
pub struct SpaceFilter {
/// The underlying [`SpaceRoom`]
space_room: SpaceRoom,
/// The level of the space filter in the tree/hierarchy.
/// At this point in time the filters are limited to the first 2 levels.
level: u8,
/// The room identifiers of the descendants of this space.
/// For top level spaces (level 0) these will be direct descendants while
/// for first level spaces they will be all other descendants, recursively.
descendants: Vec<String>,
}
impl From<UISpaceFilter> for SpaceFilter {
fn from(filter: UISpaceFilter) -> Self {
SpaceFilter {
space_room: filter.space_room.into(),
level: filter.level,
descendants: filter.descendants.into_iter().map(|id| id.to_string()).collect(),
}
}
}
+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;
@@ -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);
@@ -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,8 +1,22 @@
// 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_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter,
TimelineReadReceiptTracking,
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
};
use ruma::{
events::{AnySyncTimelineEvent, TimelineEventType},
@@ -15,31 +29,50 @@ use crate::{
event::{MessageLikeEventType, RoomMessageEventMessageType, StateEventType},
};
/// A timeline filter that includes or excludes events based on their type or
/// content.
#[derive(uniffi::Object)]
pub struct TimelineEventTypeFilter {
inner: InnerTimelineEventTypeFilter,
pub struct TimelineEventFilter {
inner: InnerTimelineEventFilter,
}
#[matrix_sdk_ffi_macros::export]
impl TimelineEventTypeFilter {
impl TimelineEventFilter {
#[uniffi::constructor]
pub fn include(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Include(event_types) })
pub fn include(conditions: Vec<FilterTimelineEventCondition>) -> Arc<Self> {
let conditions: Vec<TimelineEventCondition> =
conditions.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventFilter::Include(conditions) })
}
#[uniffi::constructor]
pub fn exclude(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let event_types: Vec<TimelineEventType> =
event_types.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventTypeFilter::Exclude(event_types) })
pub fn include_event_types(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let conditions = event_types
.iter()
.map(|t| TimelineEventCondition::EventType(t.clone().into()))
.collect();
Arc::new(Self { inner: InnerTimelineEventFilter::Include(conditions) })
}
#[uniffi::constructor]
pub fn exclude(conditions: Vec<FilterTimelineEventCondition>) -> Arc<Self> {
let conditions: Vec<TimelineEventCondition> =
conditions.iter().map(|t| t.clone().into()).collect();
Arc::new(Self { inner: InnerTimelineEventFilter::Exclude(conditions) })
}
#[uniffi::constructor]
pub fn exclude_event_types(event_types: Vec<FilterTimelineEventType>) -> Arc<Self> {
let conditions = event_types
.iter()
.map(|t| TimelineEventCondition::EventType(t.clone().into()))
.collect();
Arc::new(Self { inner: InnerTimelineEventFilter::Exclude(conditions) })
}
}
impl TimelineEventTypeFilter {
/// Filters an [`event`] to decide whether it should be part of the timeline
/// based on [`AnySyncTimelineEvent::event_type()`].
impl TimelineEventFilter {
/// Filters an `event` to decide whether it should be part of the timeline.
pub(crate) fn filter(&self, event: &AnySyncTimelineEvent) -> bool {
self.inner.filter(event)
}
@@ -64,6 +97,31 @@ impl From<FilterTimelineEventType> for TimelineEventType {
}
}
/// A condition that matches on an event's type or content.
#[derive(uniffi::Enum, Clone)]
pub enum FilterTimelineEventCondition {
/// The event has the specified event type.
EventType { event_type: FilterTimelineEventType },
/// The event is an `m.room.member` event that represents a membership
/// change (join, leave, etc.).
MembershipChange,
/// The event is an `m.room.member` event that represents a profile
/// change (displayname or avatar URL).
ProfileChange,
}
impl From<FilterTimelineEventCondition> for TimelineEventCondition {
fn from(value: FilterTimelineEventCondition) -> Self {
match value {
FilterTimelineEventCondition::EventType { event_type } => {
Self::EventType(event_type.into())
}
FilterTimelineEventCondition::MembershipChange => Self::MembershipChange,
FilterTimelineEventCondition::ProfileChange => Self::ProfileChange,
}
}
}
#[derive(uniffi::Enum)]
pub enum TimelineFocus {
Live {
@@ -76,17 +134,14 @@ pub enum TimelineFocus {
event_id: String,
/// The number of context events to load around the focused event.
num_context_events: u16,
/// Whether to hide in-thread replies from the live timeline.
hide_threaded_events: bool,
/// How to handle threaded events.
thread_mode: TimelineEventFocusThreadMode,
},
Thread {
/// The thread root event ID to focus on.
root_event_id: String,
},
PinnedEvents {
max_events_to_load: u16,
max_concurrent_requests: u16,
},
PinnedEvents,
}
impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
@@ -97,18 +152,14 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
) -> Result<matrix_sdk_ui::timeline::TimelineFocus, Self::Error> {
match value {
TimelineFocus::Live { hide_threaded_events } => Ok(Self::Live { hide_threaded_events }),
TimelineFocus::Event { event_id, num_context_events, hide_threaded_events } => {
TimelineFocus::Event { event_id, num_context_events, thread_mode } => {
let parsed_event_id =
EventId::parse(&event_id).map_err(|err| FocusEventError::InvalidEventId {
event_id: event_id.clone(),
err: err.to_string(),
})?;
Ok(Self::Event {
target: parsed_event_id,
num_context_events,
hide_threaded_events,
})
Ok(Self::Event { target: parsed_event_id, num_context_events, thread_mode })
}
TimelineFocus::Thread { root_event_id } => {
let parsed_root_event_id = EventId::parse(&root_event_id).map_err(|err| {
@@ -120,9 +171,7 @@ impl TryFrom<TimelineFocus> for matrix_sdk_ui::timeline::TimelineFocus {
Ok(Self::Thread { root_event_id: parsed_root_event_id })
}
TimelineFocus::PinnedEvents { max_events_to_load, max_concurrent_requests } => {
Ok(Self::PinnedEvents { max_events_to_load, max_concurrent_requests })
}
TimelineFocus::PinnedEvents => Ok(Self::PinnedEvents),
}
}
}
@@ -154,8 +203,8 @@ pub enum TimelineFilter {
/// appear in the timeline.
types: Vec<RoomMessageEventMessageType>,
},
/// Show only events which match this filter.
EventTypeFilter { filter: Arc<TimelineEventTypeFilter> },
/// Show only events which match this event filter.
EventFilter { filter: Arc<TimelineEventFilter> },
}
/// Various options used to configure the timeline's behavior.
+164 -44
View File
@@ -17,10 +17,13 @@ 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,
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, StateEventContentChange,
};
use crate::{client::JoinRule, timeline::msg_like::MsgLikeContent, utils::Timestamp};
use crate::{
client::JoinRule, event::TimelineEventType, ruma::AssetType,
timeline::msg_like::MsgLikeContent, utils::Timestamp,
};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
@@ -41,7 +44,7 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
Content::MembershipChange(membership) => {
let reason = match membership.content() {
FullStateEventContent::Original { content, .. } => content.reason.clone(),
StateEventContentChange::Original { content, .. } => content.reason.clone(),
_ => None,
};
TimelineItemContent::RoomMembership {
@@ -130,8 +133,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,
@@ -242,36 +245,120 @@ impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
}
}
#[derive(Clone, uniffi::Record)]
pub struct PowerLevelChanges {
ban: i64,
kick: i64,
events_default: i64,
invite: i64,
redact: i64,
state_default: i64,
users_default: i64,
notifications: i64,
}
#[derive(Clone, uniffi::Enum)]
#[allow(clippy::large_enum_variant)]
// Added because the RoomPowerLevels variant is quite large.
// This is the same issue than for TimelineItemContent.
pub enum OtherState {
PolicyRuleRoom,
PolicyRuleServer,
PolicyRuleUser,
RoomAliases,
RoomAvatar { url: Option<String> },
RoomAvatar {
url: Option<String>,
},
RoomCanonicalAlias,
RoomCreate { federate: Option<bool> },
RoomCreate {
federate: bool,
},
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility { history_visibility: Option<HistoryVisibility> },
RoomJoinRules { join_rule: Option<JoinRule> },
RoomName { name: Option<String> },
RoomPinnedEvents { change: RoomPinnedEventsChange },
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
RoomHistoryVisibility {
history_visibility: HistoryVisibility,
},
RoomJoinRules {
join_rule: Option<JoinRule>,
},
RoomName {
name: Option<String>,
},
RoomPinnedEvents {
change: RoomPinnedEventsChange,
},
RoomPowerLevels {
events: HashMap<TimelineEventType, i64>,
previous_events: Option<HashMap<TimelineEventType, i64>>,
users: HashMap<String, i64>,
previous_users: Option<HashMap<String, i64>>,
thresholds: PowerLevelChanges,
previous_thresholds: Option<PowerLevelChanges>,
},
RoomServerAcl,
RoomThirdPartyInvite { display_name: Option<String> },
RoomThirdPartyInvite {
display_name: Option<String>,
},
RoomTombstone,
RoomTopic { topic: Option<String> },
RoomTopic {
topic: Option<String>,
},
SpaceChild,
SpaceParent,
Custom { event_type: String },
Custom {
event_type: String,
},
}
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,
@@ -289,8 +376,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 }
}
@@ -298,25 +385,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 }
}
@@ -328,20 +412,56 @@ 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 {
users: power_level_user_changes(content, prev_content)
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()
.map(|(k, &v)| (k.clone().into(), v.into()))
.collect(),
previous_events: prev_content.as_ref().map(|prev_content| {
prev_content
.events
.iter()
.map(|(k, &v)| (k.clone().into(), v.into()))
.collect()
}),
thresholds: PowerLevelChanges {
ban: content.ban.into(),
kick: content.kick.into(),
events_default: content.events_default.into(),
invite: content.invite.into(),
redact: content.redact.into(),
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(),
kick: prev_content.kick.into(),
events_default: prev_content.events_default.into(),
invite: prev_content.invite.into(),
redact: prev_content.redact.into(),
state_default: prev_content.state_default.into(),
users_default: prev_content.users_default.into(),
notifications: prev_content.notifications.room.into(),
}
}),
users: power_level_user_changes(&content, &prev_content)
.iter()
.map(|(k, v)| (k.to_string(), *v))
.collect(),
previous: prev_content.as_ref().map(|prev_content| {
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 { users: Default::default(), previous: None }
}
},
}
Content::RoomServerAcl(_) => Self::RoomServerAcl,
Content::RoomThirdPartyInvite(c) => {
let display_name = match c {
+55 -27
View File
@@ -21,8 +21,7 @@ use matrix_sdk::{
attachment::{
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
event_cache::RoomPaginationStatus,
event_cache::PaginationStatus,
room::edit::EditedContent as SdkEditedContent,
};
use matrix_sdk_common::{
@@ -31,8 +30,10 @@ use matrix_sdk_common::{
};
use matrix_sdk_ui::timeline::{
self, AttachmentConfig, AttachmentSource, EventItemOrigin,
LatestEventValue as UiLatestEventValue, MediaUploadProgress as SdkMediaUploadProgress, Profile,
TimelineDetails, TimelineUniqueId as SdkTimelineUniqueId,
LatestEventValue as UiLatestEventValue, LatestEventValueLocalState,
MediaUploadProgress as SdkMediaUploadProgress, Profile, TimelineDetails,
TimelineEventShieldState as SdkShieldState, TimelineEventShieldStateCode,
TimelineUniqueId as SdkTimelineUniqueId,
};
use mime::Mime;
use reply::{EmbeddedEventDetails, InReplyToDetails};
@@ -60,8 +61,7 @@ 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,
@@ -78,6 +78,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};
@@ -722,7 +723,7 @@ impl Timeline {
/// pinned.
async fn pin_event(&self, event_id: String) -> Result<bool, ClientError> {
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
self.inner.pin_event(&event_id).await.map_err(ClientError::from)
self.inner.room().pin_event(&event_id).await.map_err(ClientError::from)
}
/// Adds a new pinned event by sending an updated `m.room.pinned_events`
@@ -732,7 +733,7 @@ impl Timeline {
/// pinned
async fn unpin_event(&self, event_id: String) -> Result<bool, ClientError> {
let event_id = EventId::parse(event_id).map_err(ClientError::from)?;
self.inner.unpin_event(&event_id).await.map_err(ClientError::from)
self.inner.room().unpin_event(&event_id).await.map_err(ClientError::from)
}
pub fn create_message_content(
@@ -820,7 +821,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)]
@@ -979,12 +980,12 @@ impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
/// authenticity properties.
#[derive(uniffi::Enum, Clone)]
pub enum ShieldState {
/// A red shield with a tooltip containing the associated message should be
/// presented.
Red { code: ShieldStateCode, message: String },
/// A grey shield with a tooltip containing the associated message should be
/// presented.
Grey { code: ShieldStateCode, message: String },
/// A red shield with a tooltip containing a message appropriate to the
/// associated code should be presented.
Red { code: TimelineEventShieldStateCode },
/// A grey shield with a tooltip containing a message appropriate to the
/// associated code should be presented.
Grey { code: TimelineEventShieldStateCode },
/// No shield should be presented.
None,
}
@@ -992,12 +993,8 @@ pub enum ShieldState {
impl From<SdkShieldState> for ShieldState {
fn from(value: SdkShieldState) -> Self {
match value {
SdkShieldState::Red { code, message } => {
Self::Red { code, message: message.to_owned() }
}
SdkShieldState::Grey { code, message } => {
Self::Grey { code, message: message.to_owned() }
}
SdkShieldState::Red { code } => Self::Red { code },
SdkShieldState::Grey { code } => Self::Grey { code },
SdkShieldState::None => Self::None,
}
}
@@ -1010,6 +1007,8 @@ pub struct EventTimelineItem {
event_or_transaction_id: EventOrTransactionId,
sender: String,
sender_profile: ProfileDetails,
forwarder: Option<String>,
forwarder_profile: Option<ProfileDetails>,
is_own: bool,
is_editable: bool,
content: TimelineItemContent,
@@ -1033,6 +1032,8 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
event_or_transaction_id: item.identifier().into(),
sender: item.sender().to_string(),
sender_profile: item.sender_profile().clone().into(),
forwarder: item.forwarder().map(ToString::to_string),
forwarder_profile: item.forwarder_profile().map(Into::into),
is_own: item.is_own(),
is_editable: item.is_editable(),
content: item.content().clone().into(),
@@ -1088,6 +1089,21 @@ impl From<TimelineDetails<Profile>> for ProfileDetails {
}
}
impl From<&TimelineDetails<Profile>> for ProfileDetails {
fn from(details: &TimelineDetails<Profile>) -> Self {
match details {
TimelineDetails::Unavailable => Self::Unavailable,
TimelineDetails::Pending => Self::Pending,
TimelineDetails::Ready(profile) => Self::Ready {
display_name: profile.display_name.clone(),
display_name_ambiguous: profile.display_name_ambiguous,
avatar_url: profile.avatar_url.as_ref().map(ToString::to_string),
},
TimelineDetails::Error(e) => Self::Error { message: e.to_string() },
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct PollData {
question: String,
@@ -1276,8 +1292,8 @@ pub struct LazyTimelineItemProvider(Arc<matrix_sdk_ui::timeline::EventTimelineIt
#[matrix_sdk_ffi_macros::export]
impl LazyTimelineItemProvider {
/// Returns the shields for this event timeline item.
fn get_shields(&self, strict: bool) -> Option<ShieldState> {
self.0.get_shield(strict).map(Into::into)
fn get_shields(&self, strict: bool) -> ShieldState {
self.0.get_shield(strict).into()
}
/// Returns some debug information for this event timeline item.
@@ -1301,7 +1317,7 @@ impl LazyTimelineItemProvider {
}
/// Mimic the [`UiLatestEventValue`] type.
#[derive(Clone, uniffi::Enum)]
#[derive(uniffi::Enum)]
pub enum LatestEventValue {
None,
Remote {
@@ -1311,12 +1327,17 @@ pub enum LatestEventValue {
profile: ProfileDetails,
content: TimelineItemContent,
},
RemoteInvite {
timestamp: Timestamp,
inviter: Option<String>,
inviter_profile: ProfileDetails,
},
Local {
timestamp: Timestamp,
sender: String,
profile: ProfileDetails,
content: TimelineItemContent,
is_sending: bool,
state: LatestEventValueLocalState,
},
}
@@ -1333,13 +1354,20 @@ impl From<UiLatestEventValue> for LatestEventValue {
content: content.into(),
}
}
UiLatestEventValue::Local { timestamp, sender, profile, content, is_sending } => {
UiLatestEventValue::RemoteInvite { timestamp, inviter, inviter_profile } => {
Self::RemoteInvite {
timestamp: timestamp.into(),
inviter: inviter.map(|inviter| inviter.to_string()),
inviter_profile: inviter_profile.into(),
}
}
UiLatestEventValue::Local { timestamp, sender, profile, content, state } => {
Self::Local {
timestamp: timestamp.into(),
sender: sender.to_string(),
profile: profile.into(),
content: content.into(),
is_sending,
state,
}
}
}
@@ -18,7 +18,7 @@ use matrix_sdk_base::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
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,
}
}
})
}
}
@@ -255,6 +289,11 @@ pub struct PollAnswer {
pub struct ThreadSummary {
pub latest_event: EmbeddedEventDetails,
pub num_replies: u32,
/// The user's own public read receipt event id, for this particular thread.
pub public_read_receipt_event_id: Option<String>,
/// The user's own private read receipt event id, for this particular
/// thread.
pub private_read_receipt_event_id: Option<String>,
}
#[matrix_sdk_ffi_macros::export]
@@ -273,6 +312,10 @@ impl From<matrix_sdk_ui::timeline::ThreadSummary> for ThreadSummary {
Self {
latest_event: EmbeddedEventDetails::from(value.latest_event),
num_replies: value.num_replies,
public_read_receipt_event_id: value.public_read_receipt_event_id.map(|v| v.to_string()),
private_read_receipt_event_id: value
.private_read_receipt_event_id
.map(|v| v.to_string()),
}
}
}
@@ -0,0 +1,144 @@
// 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 matrix_sdk::room::{
ListThreadsOptions as SdkListThreadsOptions, ThreadSubscription as SdkThreadSubscription,
};
use matrix_sdk_ui::timeline::threads::{
ThreadList as UIThreadList, ThreadListItem as UIThreadListItem,
};
use ruma::api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads;
use crate::{
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,
}
}
}
/// A structure wrapping a Thread List endpoint response i.e.
/// [`ThreadListItem`]s and the current pagination token.
#[derive(uniffi::Record)]
pub struct ThreadList {
/// The events that are thread roots in the current batch.
pub items: Vec<ThreadListItem>,
/// Token to paginate backwards in a subsequent query to
/// [`Room::list_threads`].
pub prev_batch_token: Option<String>,
}
impl From<UIThreadList> for ThreadList {
fn from(list: UIThreadList) -> Self {
Self {
items: list.items.into_iter().map(Into::into).collect(),
prev_batch_token: list.prev_batch_token,
}
}
}
/// An individual Thread as retrieved from through Thread List API.
#[derive(uniffi::Record)]
pub struct ThreadListItem {
root_event_id: String,
timestamp: Timestamp,
sender: String,
sender_profile: ProfileDetails,
is_own: bool,
content: Option<TimelineItemContent>,
}
impl From<UIThreadListItem> for ThreadListItem {
fn from(root: UIThreadListItem) -> Self {
Self {
root_event_id: root.root_event_id.to_string(),
timestamp: root.timestamp.into(),
sender: root.sender.to_string(),
is_own: root.is_own,
sender_profile: root.sender_profile.into(),
content: root.content.map(Into::into),
}
}
}
+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, Mutex};
use language_tags::LanguageTag;
+3
View File
@@ -2,3 +2,6 @@
package_name = "org.matrix.rustcomponents.sdk"
cdylib_name = "matrix_sdk_ffi"
android_cleaner = true
# Checksums are incorrectly failing for users with 32bit (and sometimes 64bit?) devices at the moment
# Remove once https://github.com/mozilla/uniffi-rs/issues/2740 is fixed or JNI is used instead of JNA
omit_checksums = true
+114 -45
View File
@@ -6,13 +6,78 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
### Bug Fixes
- 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.
([#6126](https://github.com/matrix-org/matrix-rust-sdk/pull/6126))
- [**breaking**] `BaseClient::room_info_notable_update_sender` has
moved into `BaseStateStore`. `BaseStateStore::derive_from_other`
and `BaseStateStore::get_or_create_room` no longer takes a
`room_info_notable_update_sender` argument.
([#6130](https://github.com/matrix-org/matrix-rust-sdk/pull/6130))
- [**breaking**] New `LatestEventValue::LocalHasBeenSent` variant to represent
a local event that has been sent successfully.
([#5968](https://github.com/matrix-org/matrix-rust-sdk/pull/5968))
### 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:
OwnedEventId` field.
([#5977](https://github.com/matrix-org/matrix-rust-sdk/pull/5977))
- [**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**] `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
been eliminated entirely.
([#5963](https://github.com/matrix-org/matrix-rust-sdk/pull/5963))
## [0.16.0] - 2025-12-04
### Security Fixes
- 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))
([#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
@@ -22,8 +87,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`.
@@ -44,11 +109,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
@@ -71,6 +138,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`.
@@ -117,6 +185,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
@@ -154,9 +223,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
@@ -192,17 +261,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
@@ -242,8 +311,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.
@@ -256,17 +325,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`.
@@ -276,8 +345,7 @@ No notable changes in this release.
by a custom one.
- Introduce a `DisplayName` struct which normalizes and sanitizes
display names.
display names.
### Refactor
@@ -317,34 +385,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
+1 -6
View File
@@ -50,9 +50,6 @@ test-send-sync = [
"matrix-sdk-crypto?/test-send-sync",
]
# "message-ids" feature doesn't do anything and is deprecated.
message-ids = []
# helpers for testing features build upon this
testing = [
"dep:assert_matches",
@@ -73,7 +70,7 @@ assert_matches = { workspace = true, optional = true }
assert_matches2 = { workspace = true, optional = true }
async-trait.workspace = true
bitflags = { workspace = true, features = ["serde"] }
decancer = "3.3.3"
decancer = { version = "3.3.3", default-features = false }
eyeball = { workspace = true, features = ["async-lock"] }
eyeball-im.workspace = true
futures-util.workspace = true
@@ -83,10 +80,8 @@ 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 = [
"canonical-json",
"unstable-msc2867",
"unstable-msc3381",
"unstable-msc4186",
+155 -164
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,
/// );
/// ```
@@ -112,10 +116,6 @@ pub struct BaseClient {
/// Observable of when a user is ignored/unignored.
pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
/// A sender that is used to communicate changes to room information. Each
/// tick contains the room ID and the reasons that have generated this tick.
pub(crate) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
/// The strategy to use for picking recipient devices, when sending an
/// encrypted message.
#[cfg(feature = "e2e-encryption")]
@@ -179,18 +179,6 @@ impl BaseClient {
pub fn new(config: StoreConfig, threading_support: ThreadingSupport) -> Self {
let store = BaseStateStore::new(config.state_store);
// Create the channel to receive `RoomInfoNotableUpdate`.
//
// Let's consider the channel will receive 5 updates for 100 rooms maximum. This
// is unrealistic in practise, as the sync mechanism is pretty unlikely to
// trigger such amount of updates, it's a safe value.
//
// Also, note that it must not be
// zero, because (i) it will panic, (ii) a new user has no room, but can create
// rooms; remember that the channel's capacity is immutable.
let (room_info_notable_update_sender, _room_info_notable_update_receiver) =
broadcast::channel(500);
BaseClient {
state_store: store,
event_cache_store: config.event_cache_store,
@@ -200,7 +188,6 @@ impl BaseClient {
#[cfg(feature = "e2e-encryption")]
olm_machine: Default::default(),
ignore_user_list_changes: Default::default(),
room_info_notable_update_sender,
#[cfg(feature = "e2e-encryption")]
room_key_recipient_strategy: Default::default(),
#[cfg(feature = "e2e-encryption")]
@@ -218,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,16 +224,13 @@ impl BaseClient {
crypto_store: self.crypto_store.clone(),
olm_machine: self.olm_machine.clone(),
ignore_user_list_changes: Default::default(),
room_info_notable_update_sender: self.room_info_notable_update_sender.clone(),
room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
decryption_settings: self.decryption_settings.clone(),
handle_verification_events,
threading_support: self.threading_support,
};
copy.state_store
.derive_from_other(&self.state_store, &copy.room_info_notable_update_sender)
.await?;
copy.state_store.derive_from_other(&self.state_store).await?;
Ok(copy)
}
@@ -258,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))
}
@@ -296,11 +278,7 @@ impl BaseClient {
/// Lookup the Room for the given RoomId, or create one, if it didn't exist
/// yet in the store
pub fn get_or_create_room(&self, room_id: &RoomId, room_state: RoomState) -> Room {
self.state_store.get_or_create_room(
room_id,
room_state,
self.room_info_notable_update_sender.clone(),
)
self.state_store.get_or_create_room(room_id, room_state)
}
/// Get a reference to the state store.
@@ -366,13 +344,7 @@ impl BaseClient {
) -> Result<()> {
debug!(user_id = ?session_meta.user_id, device_id = ?session_meta.device_id, "Activating the client");
self.state_store
.load_rooms(
&session_meta.user_id,
room_load_settings,
&self.room_info_notable_update_sender,
)
.await?;
self.state_store.load_rooms(&session_meta.user_id, room_load_settings).await?;
self.state_store.load_sync_token().await?;
self.state_store.set_session_meta(session_meta);
@@ -418,11 +390,7 @@ impl BaseClient {
///
/// Update the internal and cached state accordingly. Return the final Room.
pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
let room = self.state_store.get_or_create_room(
room_id,
RoomState::Knocked,
self.room_info_notable_update_sender.clone(),
);
let room = self.state_store.get_or_create_room(room_id, RoomState::Knocked);
if room.state() != RoomState::Knocked {
let _state_store_lock = self.state_store_lock().lock().await;
@@ -431,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
@@ -459,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!();
@@ -483,11 +460,7 @@ impl BaseClient {
room_id: &RoomId,
inviter: Option<OwnedUserId>,
) -> Result<Room> {
let room = self.state_store.get_or_create_room(
room_id,
RoomState::Joined,
self.room_info_notable_update_sender.clone(),
);
let room = self.state_store.get_or_create_room(room_id, RoomState::Joined);
// If the state isn't `RoomState::Joined` then this means that we knew about
// this room before. Let's modify the existing state now.
@@ -495,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();
@@ -536,11 +514,7 @@ impl BaseClient {
///
/// Update the internal and cached state accordingly.
pub async fn room_left(&self, room_id: &RoomId) -> Result<()> {
let room = self.state_store.get_or_create_room(
room_id,
RoomState::Left,
self.room_info_notable_update_sender.clone(),
);
let room = self.state_store.get_or_create_room(room_id, RoomState::Left);
if room.state() != RoomState::Left {
let _state_store_lock = self.state_store_lock().lock().await;
@@ -549,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
@@ -605,41 +587,26 @@ impl BaseClient {
let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
let user_id = self
.session_meta()
.expect("Sync shouldn't run without an authenticated user")
.user_id
.to_owned();
#[cfg(feature = "e2e-encryption")]
let olm_machine = self.olm_machine().await;
let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
#[cfg(feature = "e2e-encryption")]
let to_device = {
let processors::e2ee::to_device::Output {
processed_to_device_events: to_device,
room_key_updates,
} = processors::e2ee::to_device::from_sync_v2(
let processors::e2ee::to_device::Output { processed_to_device_events: to_device } =
processors::e2ee::to_device::from_sync_v2(
&response,
olm_machine.as_ref(),
&self.decryption_settings,
)
.await?;
processors::latest_event::decrypt_from_rooms(
&mut context,
room_key_updates
.into_iter()
.flatten()
.filter_map(|room_key_info| self.get_room(&room_key_info.room_id))
.collect(),
processors::e2ee::E2EE::new(
olm_machine.as_ref(),
&self.decryption_settings,
self.handle_verification_events,
),
)
.await?;
to_device
};
#[cfg(not(feature = "e2e-encryption"))]
let to_device = response
.to_device
@@ -682,12 +649,18 @@ 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,
processors::room::RoomCreationData::new(
&room_id,
self.room_info_notable_update_sender.clone(),
requested_required_states,
&mut ambiguity_cache,
),
@@ -699,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?;
@@ -715,7 +684,6 @@ impl BaseClient {
&mut context,
processors::room::RoomCreationData::new(
&room_id,
self.room_info_notable_update_sender.clone(),
requested_required_states,
&mut ambiguity_cache,
),
@@ -726,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?;
@@ -741,13 +705,15 @@ impl BaseClient {
let invited_room_update = processors::room::sync_v2::update_invited_room(
&mut context,
&room_id,
&user_id,
invited_room,
self.room_info_notable_update_sender.clone(),
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
&e2ee_context,
)
.await?;
@@ -758,13 +724,15 @@ impl BaseClient {
let knocked_room_update = processors::room::sync_v2::update_knocked_room(
&mut context,
&room_id,
&user_id,
knocked_room,
self.room_info_notable_update_sender.clone(),
processors::notification::Notification::new(
&push_rules,
&mut notifications,
&self.state_store,
),
#[cfg(feature = "e2e-encryption")]
&e2ee_context,
)
.await?;
@@ -1029,11 +997,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?)
}
@@ -1120,7 +1090,7 @@ impl BaseClient {
///
/// Learn more by reading the [`RoomInfoNotableUpdate`] type.
pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
self.room_info_notable_update_sender.subscribe()
self.state_store.room_info_notable_update_sender.subscribe()
}
/// Checks whether the provided `user_id` belongs to an ignored user.
@@ -1143,6 +1113,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.
@@ -1206,11 +1194,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},
@@ -1390,6 +1381,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();
@@ -1408,19 +1400,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);
@@ -1520,7 +1507,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
@@ -1582,7 +1569,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
@@ -1646,7 +1633,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
@@ -1660,23 +1647,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();
@@ -1720,7 +1697,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,
);
@@ -1780,37 +1757,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
@@ -1822,19 +1805,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,13 +14,13 @@
//! 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, OwnedEventId, OwnedRoomId, OwnedUserId, UInt, UserId,
EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedRoomId, OwnedUserId, UInt,
UserId,
events::{
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, EventContentFromType,
PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
@@ -71,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")
});
@@ -88,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")
});
@@ -96,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")
});
@@ -104,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")
});
@@ -486,16 +486,34 @@ impl MemberEvent {
self.state_key()
}
/// The value of the `displayname` field in this member event.
///
/// [`MemberEvent::display_name()`] should be preferred to get the name to
/// display for this member event.
pub fn displayname_value(&self) -> Option<&str> {
match self {
Self::Sync(event) => event.as_original()?.content.displayname.as_deref(),
Self::Stripped(event) => event.content.displayname.as_deref(),
}
}
/// The name that should be displayed for this member event.
///
/// It there is no `displayname` in the event's content, the localpart or
/// the user ID is returned.
pub fn display_name(&self) -> DisplayName {
DisplayName::new(
self.original_content()
.and_then(|c| c.displayname.as_deref())
.unwrap_or_else(|| self.user_id().localpart()),
)
DisplayName::new(self.displayname_value().unwrap_or_else(|| self.user_id().localpart()))
}
/// The URL of the avatar in this member event.
///
/// [`MemberEvent::display_name()`] should be preferred to get the name to
/// display for this member event.
pub fn avatar_url(&self) -> Option<&MxcUri> {
match self {
Self::Sync(event) => event.as_original()?.content.avatar_url.as_deref(),
Self::Stripped(event) => event.content.avatar_url.as_deref(),
}
}
/// The optional reason why the membership changed.
@@ -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,
}
File diff suppressed because it is too large Load Diff
@@ -13,7 +13,7 @@
// limitations under the License.
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::{Arc, RwLock as StdRwLock},
};
@@ -37,6 +37,12 @@ use crate::event_cache::{Event, Gap};
/// In-memory, non-persistent implementation of the `EventCacheStore`.
///
/// Default if no other is configured at startup.
///
/// Note that this store is not transactional. This is particularly
/// relevant when calling [`EventCacheStore::handle_linked_chunk_updates`],
/// which consumes a list of [`Update`]s. When processing this list, if
/// one of the [`Update`]s fails, the previous updates in the list
/// will not be reversed.
#[derive(Debug, Clone)]
pub struct MemoryStore {
inner: Arc<StdRwLock<MemoryStoreInner>>,
@@ -88,7 +94,10 @@ impl EventCacheStore for MemoryStore {
updates: Vec<Update<Event, Gap>>,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
inner.events.apply_updates(linked_chunk_id, updates);
inner
.events
.apply_updates(linked_chunk_id, updates)
.map_err(|e| Self::Error::Backend(Arc::new(e)))?;
Ok(())
}
@@ -179,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)
}
@@ -195,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());
@@ -210,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(
@@ -228,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)
}
@@ -271,5 +312,6 @@ mod tests {
}
event_cache_store_integration_tests!();
#[cfg(not(target_family = "wasm"))]
event_cache_store_integration_tests_time!();
}
@@ -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))
}
}
+140 -601
View File
@@ -1,28 +1,10 @@
//! Utilities for working with events to decide whether they are suitable for
//! use as a [crate::Room::latest_event].
//! The Latest Event basic types.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId};
#[cfg(feature = "e2e-encryption")]
use ruma::{
UserId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent,
call::invite::SyncCallInviteEvent,
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
message::{MessageType, SyncRoomMessageEvent},
power_levels::RoomPowerLevels,
},
rtc::notification::SyncRtcNotificationEvent,
sticker::SyncStickerEvent,
},
};
use ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId};
use serde::{Deserialize, Serialize};
use crate::{MinimalRoomMemberEvent, store::SerializableEventContent};
use crate::store::SerializableEventContent;
/// A latest event value!
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
@@ -34,9 +16,32 @@ pub enum LatestEventValue {
/// The latest event represents a remote event.
Remote(RemoteLatestEventValue),
/// The latest event represents an invite, i.e. the current user has been
/// invited to join a room.
RemoteInvite {
/// The ID of the invite event.
event_id: Option<OwnedEventId>,
/// The timestamp of the invite event.
timestamp: MilliSecondsSinceUnixEpoch,
/// The user ID of the inviter.
inviter: Option<OwnedUserId>,
},
/// The latest event represents a local event that is sending.
LocalIsSending(LocalLatestEventValue),
/// The latest event represents a local event that has been sent
/// successfully. It should come quickly as a [`Self::Remote`].
LocalHasBeenSent {
/// ID of the sent event.
event_id: OwnedEventId,
/// Value, as for other `Self::Local*` variants.
value: LocalLatestEventValue,
},
/// The latest event represents a local event that cannot be sent, either
/// because a previous local event, or this local event cannot be sent.
LocalCannotBeSent(LocalLatestEventValue),
@@ -45,20 +50,28 @@ pub enum LatestEventValue {
impl LatestEventValue {
/// Get the timestamp of the [`LatestEventValue`].
///
/// If it's [`None`], it returns `None`. If it's [`Remote`], it returns the
/// [`TimelineEvent::timestamp`]. If it's [`LocalIsSending`] or
/// [`LocalCannotBeSent`], it returns the
/// [`LocalLatestEventValue::timestamp`] value.
/// - If it's [`None`], it returns `None`.
/// - If it's [`Remote`], it returns the [`TimelineEvent::timestamp`].
/// - If it's [`RemoteInvite`], it returns the
/// [`SyncOrStrippedState::timestamp`].
/// - If it's [`LocalIsSending`],[`LocalHasBeenSent`] or
/// [`LocalCannotBeSent`], it returns the
/// [`LocalLatestEventValue::timestamp`] value.
///
/// [`None`]: LatestEventValue::None
/// [`Remote`]: LatestEventValue::Remote
/// [`RemoteInvite`]: LatestEventValue::RemoteInvite
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
/// [`LocalHasBeenSent`]: LatestEventValue::LocalHasBeenSent
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
/// [`SyncOrStrippedState::timestamp`]: crate::deserialized_responses::SyncOrStrippedState::timestamp
pub fn timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
match self {
Self::None => None,
Self::Remote(remote_latest_event_value) => remote_latest_event_value.timestamp(),
Self::RemoteInvite { timestamp, .. } => Some(*timestamp),
Self::LocalIsSending(LocalLatestEventValue { timestamp, .. })
| Self::LocalHasBeenSent { value: LocalLatestEventValue { timestamp, .. }, .. }
| Self::LocalCannotBeSent(LocalLatestEventValue { timestamp, .. }) => Some(*timestamp),
}
}
@@ -69,9 +82,44 @@ impl LatestEventValue {
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
pub fn is_local(&self) -> bool {
match self {
Self::LocalIsSending(_)
| Self::LocalHasBeenSent { .. }
| Self::LocalCannotBeSent(_) => true,
Self::None | Self::Remote(_) | Self::RemoteInvite { .. } => false,
}
}
/// Check whether the [`LatestEventValue`] represents an unsent event, i.e.
/// is [`LocalIsSending`] nor [`LocalCannotBeSent`].
///
/// [`LocalIsSending`]: LatestEventValue::LocalIsSending
/// [`LocalCannotBeSent`]: LatestEventValue::LocalCannotBeSent
pub fn is_unsent(&self) -> bool {
match self {
Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) => true,
Self::None | Self::Remote(_) => false,
Self::LocalHasBeenSent { .. }
| Self::Remote(_)
| Self::RemoteInvite { .. }
| Self::None => false,
}
}
/// Check whether the [`LatestEventValue`] is not set, i.e. [`None`].
///
/// [`None`]: LatestEventValue::None
pub fn is_none(&self) -> bool {
matches!(self, Self::None)
}
/// Get the event ID (if it exists) of the event representing the
/// [`LatestEventValue`].
pub fn event_id(&self) -> Option<OwnedEventId> {
match self {
Self::Remote(event) => event.event_id(),
Self::RemoteInvite { event_id, .. } => event_id.clone(),
Self::LocalHasBeenSent { event_id, .. } => Some(event_id.clone()),
Self::LocalIsSending(_) | Self::LocalCannotBeSent(_) | Self::None => None,
}
}
}
@@ -95,6 +143,7 @@ mod tests_latest_event_value {
use ruma::{
MilliSecondsSinceUnixEpoch,
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
owned_event_id,
serde::Raw,
uint,
};
@@ -143,6 +192,22 @@ mod tests_latest_event_value {
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
#[test]
fn test_timestamp_with_local_has_been_sent() {
let value = LatestEventValue::LocalHasBeenSent {
event_id: owned_event_id!("$ev0"),
value: LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
},
};
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
#[test]
fn test_timestamp_with_local_cannot_be_sent() {
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
@@ -155,601 +220,75 @@ mod tests_latest_event_value {
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
}
/// Represents a decision about whether an event could be stored as the latest
/// event in a room. Variants starting with Yes indicate that this message could
/// be stored, and provide the inner event information, and those starting with
/// a No indicate that it could not, and give a reason.
#[cfg(feature = "e2e-encryption")]
#[derive(Debug)]
pub enum PossibleLatestEvent<'a> {
/// This message is suitable - it is an m.room.message
YesRoomMessage(&'a SyncRoomMessageEvent),
/// This message is suitable - it is a sticker
YesSticker(&'a SyncStickerEvent),
/// This message is suitable - it is a poll
YesPoll(&'a SyncUnstablePollStartEvent),
/// This message is suitable - it is a call invite
YesCallInvite(&'a SyncCallInviteEvent),
/// This message is suitable - it's a call notification
YesRtcNotification(&'a SyncRtcNotificationEvent),
/// This state event is suitable - it's a knock membership change
/// that can be handled by the current user.
YesKnockedStateEvent(&'a SyncRoomMemberEvent),
// Later: YesState(),
// Later: YesReaction(),
/// Not suitable - it's a state event
NoUnsupportedEventType,
/// Not suitable - it's not a m.room.message or an edit/replacement
NoUnsupportedMessageLikeType,
/// Not suitable - it's encrypted
NoEncrypted,
}
/// Decide whether an event could be stored as the latest event in a room.
/// Returns a LatestEvent representing our decision.
#[cfg(feature = "e2e-encryption")]
pub fn is_suitable_for_latest_event<'a>(
event: &'a AnySyncTimelineEvent,
power_levels_info: Option<(&'a UserId, &'a RoomPowerLevels)>,
) -> PossibleLatestEvent<'a> {
match event {
// Suitable - we have an m.room.message that was not redacted or edited
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(message)) => {
if let Some(original_message) = message.as_original() {
// Don't show incoming verification requests
if let MessageType::VerificationRequest(_) = original_message.content.msgtype {
return PossibleLatestEvent::NoUnsupportedMessageLikeType;
}
// Check if this is a replacement for another message. If it is, ignore it
let is_replacement =
original_message.content.relates_to.as_ref().is_some_and(|relates_to| {
if let Some(relation_type) = relates_to.rel_type() {
relation_type == RelationType::Replacement
} else {
false
}
});
if is_replacement {
PossibleLatestEvent::NoUnsupportedMessageLikeType
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
} else {
PossibleLatestEvent::YesRoomMessage(message)
}
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(poll)) => {
PossibleLatestEvent::YesPoll(poll)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(invite)) => {
PossibleLatestEvent::YesCallInvite(invite)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RtcNotification(notify)) => {
PossibleLatestEvent::YesRtcNotification(notify)
}
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(sticker)) => {
PossibleLatestEvent::YesSticker(sticker)
}
// Encrypted events are not suitable
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(_)) => {
PossibleLatestEvent::NoEncrypted
}
// Later, if we support reactions:
// AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Reaction(_))
// MessageLike, but not one of the types we want to show in message previews, so not
// suitable
AnySyncTimelineEvent::MessageLike(_) => PossibleLatestEvent::NoUnsupportedMessageLikeType,
// We don't currently support most state events
AnySyncTimelineEvent::State(state) => {
// But we make an exception for knocked state events *if* the current user
// can either accept or decline them
if let AnySyncStateEvent::RoomMember(member) = state
&& matches!(member.membership(), MembershipState::Knock)
{
let can_accept_or_decline_knocks = match power_levels_info {
Some((own_user_id, room_power_levels)) => {
room_power_levels.user_can_invite(own_user_id)
|| room_power_levels.user_can_kick(own_user_id)
}
_ => false,
};
// The current user can act on the knock changes, so they should be
// displayed
if can_accept_or_decline_knocks {
return PossibleLatestEvent::YesKnockedStateEvent(member);
}
}
PossibleLatestEvent::NoUnsupportedEventType
}
}
}
/// Represent all information required to represent a latest event in an
/// efficient way.
///
/// ## Implementation details
///
/// Serialization and deserialization should be a breeze, but we introduced a
/// change in the format without realizing, and without a migration. Ideally,
/// this would be handled with a `serde(untagged)` enum that would be used to
/// deserialize in either the older format, or to the new format. Unfortunately,
/// untagged enums don't play nicely with `serde_json::value::RawValue`,
/// so we did have to implement a custom `Deserialize` for `LatestEvent`, that
/// first deserializes the thing as a raw JSON value, and then deserializes the
/// JSON string as one variant or the other.
///
/// Because of that, `LatestEvent` should only be (de)serialized using
/// serde_json.
///
/// Whenever you introduce new fields to `LatestEvent` make sure to add them to
/// `SerializedLatestEvent` too.
#[derive(Clone, Debug, Serialize)]
pub struct LatestEvent {
/// The actual event.
event: TimelineEvent,
/// The member profile of the event' sender.
#[serde(skip_serializing_if = "Option::is_none")]
sender_profile: Option<MinimalRoomMemberEvent>,
/// The name of the event' sender is ambiguous.
#[serde(skip_serializing_if = "Option::is_none")]
sender_name_is_ambiguous: Option<bool>,
}
#[derive(Deserialize)]
struct SerializedLatestEvent {
/// The actual event.
event: TimelineEvent,
/// The member profile of the event' sender.
#[serde(skip_serializing_if = "Option::is_none")]
sender_profile: Option<MinimalRoomMemberEvent>,
/// The name of the event' sender is ambiguous.
#[serde(skip_serializing_if = "Option::is_none")]
sender_name_is_ambiguous: Option<bool>,
}
// Note: this deserialize implementation for LatestEvent will *only* work with
// serde_json.
impl<'de> Deserialize<'de> for LatestEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: Box<serde_json::value::RawValue> = Box::deserialize(deserializer)?;
let mut variant_errors = Vec::new();
match serde_json::from_str::<SerializedLatestEvent>(raw.get()) {
Ok(value) => {
return Ok(LatestEvent {
event: value.event,
sender_profile: value.sender_profile,
sender_name_is_ambiguous: value.sender_name_is_ambiguous,
});
}
Err(err) => variant_errors.push(err),
}
match serde_json::from_str::<TimelineEvent>(raw.get()) {
Ok(value) => {
return Ok(LatestEvent {
event: value,
sender_profile: None,
sender_name_is_ambiguous: None,
});
}
Err(err) => variant_errors.push(err),
}
Err(serde::de::Error::custom(format!(
"data did not match any variant of serialized LatestEvent (using serde_json). \
Observed errors: {variant_errors:?}"
)))
}
}
impl LatestEvent {
/// Create a new [`LatestEvent`] without the sender's profile.
pub fn new(event: TimelineEvent) -> Self {
Self { event, sender_profile: None, sender_name_is_ambiguous: None }
}
/// Create a new [`LatestEvent`] with maybe the sender's profile.
pub fn new_with_sender_details(
event: TimelineEvent,
sender_profile: Option<MinimalRoomMemberEvent>,
sender_name_is_ambiguous: Option<bool>,
) -> Self {
Self { event, sender_profile, sender_name_is_ambiguous }
}
/// Transform [`Self`] into an event.
pub fn into_event(self) -> TimelineEvent {
self.event
}
/// Get a reference to the event.
pub fn event(&self) -> &TimelineEvent {
&self.event
}
/// Get a mutable reference to the event.
pub fn event_mut(&mut self) -> &mut TimelineEvent {
&mut self.event
}
/// Get the event ID.
pub fn event_id(&self) -> Option<OwnedEventId> {
self.event.event_id()
}
/// Check whether [`Self`] has a sender profile.
pub fn has_sender_profile(&self) -> bool {
self.sender_profile.is_some()
}
/// Return the sender's display name if it was known at the time [`Self`]
/// was built.
pub fn sender_display_name(&self) -> Option<&str> {
self.sender_profile.as_ref().and_then(|profile| {
profile.as_original().and_then(|event| event.content.displayname.as_deref())
})
}
/// Return `Some(true)` if the sender's name is ambiguous, `Some(false)` if
/// it isn't, `None` if ambiguity detection wasn't possible at the time
/// [`Self`] was built.
pub fn sender_name_ambiguous(&self) -> Option<bool> {
self.sender_name_is_ambiguous
}
/// Return the sender's avatar URL if it was known at the time [`Self`] was
/// built.
pub fn sender_avatar_url(&self) -> Option<&MxcUri> {
self.sender_profile.as_ref().and_then(|profile| {
profile.as_original().and_then(|event| event.content.avatar_url.as_deref())
})
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeMap;
use std::time::Duration;
#[cfg(feature = "e2e-encryption")]
use assert_matches::assert_matches;
#[cfg(feature = "e2e-encryption")]
use assert_matches2::assert_let;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_test::event_factory::EventFactory;
#[cfg(feature = "e2e-encryption")]
use ruma::{
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
events::{
SyncMessageLikeEvent,
call::{SessionDescription, invite::CallInviteEventContent},
poll::{
unstable_response::UnstablePollResponseEventContent,
unstable_start::{
NewUnstablePollStartEventContent, UnstablePollAnswer,
UnstablePollStartContentBlock,
},
},
relation::Replacement,
room::{
ImageInfo, MediaSource,
encrypted::{
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
},
message::{
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
Relation, RoomMessageEventContent,
},
topic::RoomTopicEventContent,
},
sticker::{StickerEventContent, SyncStickerEvent},
},
owned_event_id, owned_mxc_uri, user_id,
};
use ruma::{
events::rtc::notification::{NotificationType, RtcNotificationEventContent},
serde::Raw,
};
use serde_json::json;
use super::LatestEvent;
#[cfg(feature = "e2e-encryption")]
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_room_messages_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomMessageEventContent::new(MessageType::Image(ImageMessageEventContent::new(
"".to_owned(),
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
))))
.into();
assert_let!(
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event, None)
);
fn test_event_id_with_none() {
let value = LatestEventValue::None;
assert_eq!(m.content.msgtype.msgtype(), "m.image");
assert!(value.event_id().is_none());
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_polls_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
"do you like rust?",
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
)))
.into();
assert_let!(
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event, None)
);
fn test_event_id_with_remote() {
let event_id = owned_event_id!("$ev0");
let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
Raw::from_json_string(
json!({
"content": RoomMessageEventContent::text_plain("raclette"),
"type": "m.room.message",
"event_id": event_id,
"room_id": "!r0",
"origin_server_ts": 42,
"sender": "@mnt_io:matrix.org",
})
.to_string(),
)
.unwrap(),
));
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
assert_eq!(value.event_id(), Some(event_id));
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_invites_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(CallInviteEventContent::new(
"call_id".into(),
UInt::new(123).unwrap(),
SessionDescription::new("".into(), "".into()),
VoipVersionId::V1,
fn test_event_id_with_local_is_sending() {
let value = LatestEventValue::LocalIsSending(LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.into();
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
.unwrap(),
});
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_notifications_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RtcNotificationEventContent::new(
MilliSecondsSinceUnixEpoch::now(),
Duration::new(30, 0),
NotificationType::Ring,
))
.into();
assert_let!(
PossibleLatestEvent::YesRtcNotification(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_stickers_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(StickerEventContent::new(
"sticker!".to_owned(),
ImageInfo::new(),
owned_mxc_uri!("mxc://example.com/1"),
))
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesSticker(SyncStickerEvent::Original(_))
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_different_types_of_messagelike_are_unsuitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(UnstablePollResponseEventContent::new(
vec![String::from("option1")],
owned_event_id!("$1"),
))
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_redacted_messages_are_suitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.redacted(user_id!("@x:y.za"), RedactedRoomMessageEventContent::new())
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Redacted(_))
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_encrypted_messages_are_unsuitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomEncryptedEventContent::new(
EncryptedEventScheme::OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content::new(
BTreeMap::new(),
"".to_owned(),
)),
None,
))
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoEncrypted
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_state_events_are_unsuitable() {
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomTopicEventContent::new("".to_owned()))
.state_key("")
.into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedEventType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_replacement_events_are_unsuitable() {
let mut event_content = RoomMessageEventContent::text_plain("Bye bye, world!");
event_content.relates_to = Some(Relation::Replacement(Replacement::new(
owned_event_id!("$1"),
RoomMessageEventContent::text_plain("Hello, world!").into(),
)));
let event = EventFactory::new().sender(user_id!("@a:b.c")).event(event_content).into();
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_verification_requests_are_unsuitable() {
use ruma::{device_id, events::room::message::KeyVerificationRequestEventContent, user_id};
let event = EventFactory::new()
.sender(user_id!("@a:b.c"))
.event(RoomMessageEventContent::new(MessageType::VerificationRequest(
KeyVerificationRequestEventContent::new(
"body".to_owned(),
vec![],
device_id!("device_id").to_owned(),
user_id!("@user_id:example.com").to_owned(),
),
)))
.into();
assert_let!(
PossibleLatestEvent::NoUnsupportedMessageLikeType =
is_suitable_for_latest_event(&event, None)
);
assert!(value.event_id().is_none());
}
#[test]
fn test_deserialize_latest_event() {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct TestStruct {
latest_event: LatestEvent,
}
let event = TimelineEvent::from_plaintext(
Raw::from_json_string(json!({ "event_id": "$1" }).to_string()).unwrap(),
);
let initial = TestStruct {
latest_event: LatestEvent {
event: event.clone(),
sender_profile: None,
sender_name_is_ambiguous: None,
fn test_event_id_with_local_has_been_sent() {
let event_id = owned_event_id!("$ev0");
let value = LatestEventValue::LocalHasBeenSent {
event_id: event_id.clone(),
value: LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
},
};
// When serialized, LatestEvent always uses the new format.
let serialized = serde_json::to_value(&initial).unwrap();
assert_eq!(
serialized,
json!({
"latest_event": {
"event": {
"kind": {
"PlainText": {
"event": {
"event_id": "$1"
}
}
},
"thread_summary": "None",
"timestamp": null,
}
}
})
);
assert_eq!(value.event_id(), Some(event_id));
}
// And it can be properly deserialized from the new format.
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
// The previous format can also be deserialized.
let serialized = json!({
"latest_event": {
"event": {
"encryption_info": null,
"event": {
"event_id": "$1"
}
},
}
#[test]
fn test_event_id_with_local_cannot_be_sent() {
let value = LatestEventValue::LocalCannotBeSent(LocalLatestEventValue {
timestamp: MilliSecondsSinceUnixEpoch(uint!(42)),
content: SerializableEventContent::new(&AnyMessageLikeEventContent::RoomMessage(
RoomMessageEventContent::text_plain("raclette"),
))
.unwrap(),
});
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
// The even older format can also be deserialized.
let serialized = json!({
"latest_event": event
});
let deserialized: TestStruct = serde_json::from_value(serialized).unwrap();
assert_eq!(deserialized.latest_event.event().event_id().unwrap(), "$1");
assert!(deserialized.latest_event.sender_profile.is_none());
assert!(deserialized.latest_event.sender_name_is_ambiguous.is_none());
assert!(value.event_id().is_none());
}
}
+6 -9
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,21 +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, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
};
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawStateEventWithKeys};
#[cfg(test)]
matrix_sdk_test_utils::init_tracing_for_tests!();
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Media store and common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
//! Media store and common types for [media content](https://spec.matrix.org/latest/client-server-api/#content-repository).
pub mod store;
+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();
File diff suppressed because it is too large Load Diff
@@ -30,7 +30,7 @@ use crate::Result;
/// The returned [`TimelineEvent`] has no push actions set up. It's the
/// responsibility of the caller to set them.
pub async fn sync_timeline_event(
e2ee: E2EE<'_>,
e2ee: &E2EE<'_>,
event: &TimelineEvent,
room_id: &RoomId,
) -> Result<Option<TimelineEvent>> {
@@ -17,9 +17,7 @@ use std::collections::BTreeMap;
use matrix_sdk_common::deserialized_responses::{
ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo, ToDeviceUnableToDecryptReason,
};
use matrix_sdk_crypto::{
DecryptionSettings, EncryptionSyncChanges, OlmMachine, store::types::RoomKeyInfo,
};
use matrix_sdk_crypto::{DecryptionSettings, EncryptionSyncChanges, OlmMachine};
use ruma::{
OneTimeKeyAlgorithm, UInt,
api::client::sync::sync_events::{DeviceLists, v3, v5},
@@ -100,10 +98,10 @@ async fn process(
// decrypts to-device events, but leaves room events alone.
// This makes sure that we have the decryption keys for the room
// events at hand.
let (events, room_key_updates) =
let (events, _room_key_updates) =
olm_machine.receive_sync_changes(encryption_sync_changes, decryption_settings).await?;
Output { processed_to_device_events: events, room_key_updates: Some(room_key_updates) }
Output { processed_to_device_events: events }
} else {
// If we have no `OlmMachine`, just return the clear events that were passed in.
// The encrypted ones are dropped as they are un-usable.
@@ -131,12 +129,10 @@ async fn process(
}
})
.collect(),
room_key_updates: None,
}
})
}
pub struct Output {
pub processed_to_device_events: Vec<ProcessedToDeviceEvent>,
pub room_key_updates: Option<Vec<RoomKeyInfo>>,
}
@@ -1,211 +0,0 @@
// 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 the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
use super::{Context, e2ee::E2EE, verification};
use crate::{
Result, Room,
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
};
/// Decrypt any [`Room::latest_encrypted_events`] for a particular set of
/// [`Room`]s.
///
/// If we can decrypt them, change [`Room::latest_event`] to reflect what we
/// found, and remove any older encrypted events from
/// [`Room::latest_encrypted_events`].
pub async fn decrypt_from_rooms(
context: &mut Context,
rooms: Vec<Room>,
e2ee: E2EE<'_>,
) -> Result<()> {
// All functions used by this one expect an `OlmMachine`. Return if there is
// none.
if e2ee.olm_machine.is_none() {
return Ok(());
}
for room in rooms {
// Try to find a message we can decrypt and is suitable for using as the latest
// event. If we found one, set it as the latest and delete any older
// encrypted events
if let Some((found, found_index)) = find_suitable_and_decrypt(&room, &e2ee).await {
room.on_latest_event_decrypted(
found,
found_index,
&mut context.state_changes,
&mut context.room_info_notable_updates,
);
}
}
Ok(())
}
async fn find_suitable_and_decrypt(
room: &Room,
e2ee: &E2EE<'_>,
) -> Option<(Box<LatestEvent>, usize)> {
let enc_events = room.latest_encrypted_events();
let power_levels = room.power_levels().await.ok();
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
// Walk backwards through the encrypted events, looking for one we can decrypt
for (i, event) in enc_events.iter().enumerate().rev() {
// Size of the `decrypt_sync_room_event` future should not impact this
// async fn since it is likely that there aren't even any encrypted
// events when calling it.
let decrypt_sync_room_event =
Box::pin(decrypt_sync_room_event(event, e2ee, room.room_id()));
if let Ok(decrypted) = decrypt_sync_room_event.await {
// We found an event we can decrypt
if let Ok(any_sync_event) = decrypted.raw().deserialize() {
// We can deserialize it to find its type
match is_suitable_for_latest_event(&any_sync_event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_)
| PossibleLatestEvent::YesRtcNotification(_)
| PossibleLatestEvent::YesSticker(_)
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
return Some((Box::new(LatestEvent::new(decrypted)), i));
}
_ => (),
}
}
}
}
None
}
/// Attempt to decrypt the given raw event into a [`TimelineEvent`].
///
/// In the case of a decryption error, returns a [`TimelineEvent`]
/// representing the decryption error; in the case of problems with our
/// application, returns `Err`.
///
/// # Panics
///
/// Panics if there is no [`OlmMachine`] in [`E2EE`].
async fn decrypt_sync_room_event(
event: &Raw<AnySyncTimelineEvent>,
e2ee: &E2EE<'_>,
room_id: &RoomId,
) -> Result<TimelineEvent> {
let event = match e2ee
.olm_machine
.expect("An `OlmMachine` is expected")
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
// We're fine not setting the push actions for the latest event.
// TODO: we should use `TimelineEvent::to_decrypted`
// but this whole code is about to get soon removed by
// https://github.com/matrix-org/matrix-rust-sdk/pull/5624.
let event = TimelineEvent::from_decrypted(decrypted, None);
if let Ok(sync_timeline_event) = event.raw().deserialize() {
verification::process_if_relevant(&sync_timeline_event, e2ee.clone(), room_id)
.await?;
}
event
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
// TODO: we should use `TimelineEvent::to_utd`
// but this whole code is about to get soon removed by
// https://github.com/matrix-org/matrix-rust-sdk/pull/5624.
TimelineEvent::from_utd(event.clone(), utd_info)
}
};
Ok(event)
}
#[cfg(test)]
mod tests {
use matrix_sdk_test::{
JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
};
use ruma::{event_id, events::room::member::MembershipState, room_id, user_id};
use super::{Context, E2EE, decrypt_from_rooms};
use crate::{room::RoomInfoNotableUpdateReasons, test_utils::logged_in_base_client};
#[async_test]
async fn test_when_there_are_no_latest_encrypted_events_decrypting_them_does_nothing() {
// Given a room
let user_id = user_id!("@u:u.to");
let room_id = room_id!("!r:u.to");
let client = logged_in_base_client(Some(user_id)).await;
let mut sync_builder = SyncResponseBuilder::new();
let response = sync_builder
.add_joined_room(
JoinedRoomBuilder::new(room_id).add_timeline_event(
EventFactory::new()
.member(user_id)
.display_name("Alice")
.membership(MembershipState::Join)
.event_id(event_id!("$1")),
),
)
.build_sync_response();
client.receive_sync_response(response).await.unwrap();
let room = client.get_room(room_id).expect("Just-created room not found!");
// Sanity: it has no latest_encrypted_events or latest_event
assert!(room.latest_encrypted_events().is_empty());
assert!(room.latest_event().is_none());
// When I tell it to do some decryption
let mut context = Context::default();
decrypt_from_rooms(
&mut context,
vec![room.clone()],
E2EE::new(
client.olm_machine().await.as_ref(),
&client.decryption_settings,
client.handle_verification_events,
),
)
.await
.unwrap();
// Then nothing changed
assert!(room.latest_encrypted_events().is_empty());
assert!(room.latest_event().is_none());
assert!(context.state_changes.room_infos.is_empty());
assert!(
!context
.room_info_notable_updates
.get(room_id)
.copied()
.unwrap_or_default()
.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT)
);
}
}
@@ -17,8 +17,6 @@ pub mod changes;
#[cfg(feature = "e2e-encryption")]
pub mod e2ee;
pub mod ephemeral_events;
#[cfg(feature = "e2e-encryption")]
pub mod latest_event;
pub mod notification;
pub mod profiles;
pub mod room;
@@ -13,9 +13,8 @@
// limitations under the License.
use ruma::RoomId;
use tokio::sync::broadcast::Sender;
use crate::{RequestedRequiredStates, RoomInfoNotableUpdate, store::ambiguity_map::AmbiguityCache};
use crate::{RequestedRequiredStates, store::ambiguity_map::AmbiguityCache};
pub mod display_name;
pub mod msc4186;
@@ -24,7 +23,6 @@ pub mod sync_v2;
/// A classical set of data used by some processors in this module.
pub struct RoomCreationData<'a> {
room_id: &'a RoomId,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
requested_required_states: &'a RequestedRequiredStates,
ambiguity_cache: &'a mut AmbiguityCache,
}
@@ -32,15 +30,9 @@ pub struct RoomCreationData<'a> {
impl<'a> RoomCreationData<'a> {
pub fn new(
room_id: &'a RoomId,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
requested_required_states: &'a RequestedRequiredStates,
ambiguity_cache: &'a mut AmbiguityCache,
) -> Self {
Self {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
}
Self { room_id, requested_required_states, ambiguity_cache }
}
}
@@ -18,8 +18,7 @@ use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeSet;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use as_variant::as_variant;
use matrix_sdk_common::timer;
use ruma::{
JsOption, OwnedRoomId, RoomId, UserId,
@@ -29,12 +28,11 @@ use ruma::{
},
assign,
events::{
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent, StateEventType,
room::member::MembershipState,
},
serde::Raw,
};
use tokio::sync::broadcast::Sender;
#[cfg(feature = "e2e-encryption")]
use super::super::e2ee;
@@ -42,13 +40,11 @@ use super::{
super::{Context, notification, state_events, timeline},
RoomCreationData,
};
#[cfg(feature = "e2e-encryption")]
use crate::StateChanges;
use crate::{
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons,
RoomState,
Result, Room, RoomHero, RoomInfo, RoomInfoNotableUpdateReasons, RoomState,
store::BaseStateStore,
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
utils::RawStateEventWithKeys,
};
/// Represent any kind of room updates.
@@ -70,12 +66,8 @@ pub async fn update_any_room(
) -> Result<Option<(RoomInfo, RoomUpdateKind)>> {
let _timer = timer!(tracing::Level::TRACE, "update_any_room");
let RoomCreationData {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
} = room_creation_data;
let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
room_creation_data;
// Read state events from the `required_state` field.
//
@@ -83,27 +75,35 @@ pub async fn update_any_room(
// incomplete or staled already. We must only read state events from
// `required_state`.
let state = State::from_msc4186(room_response.required_state.clone());
let (raw_state_events, state_events) = state.collect(&[]);
let mut raw_state_events = state.collect(&[]);
let state_store = notification.state_store;
// Find or create the room in the store
let is_new_room = !state_store.room_exists(room_id);
let invite_state_events =
let mut raw_invite_state_events =
room_response.invite_state.as_ref().map(|events| state_events::stripped::collect(events));
#[allow(unused_mut)] // Required for some feature flag combinations
let (mut room, mut room_info, maybe_room_update_kind) = membership(
context,
&state_events,
&invite_state_events,
&mut raw_state_events,
raw_invite_state_events.as_deref_mut(),
state_store,
user_id,
room_id,
room_info_notable_update_sender,
);
// If we are not joined to the room, we cannot be waiting for a key bundle:
// clear any flag that we are.
#[cfg(feature = "e2e-encryption")]
if room_info.state() != RoomState::Joined
&& let Some(olm) = e2ee.olm_machine
{
olm.store().clear_room_pending_key_bundle(room_info.room_id()).await?
}
room_info.mark_state_partially_synced();
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
@@ -115,23 +115,24 @@ pub async fn update_any_room(
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
raw_state_events,
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
state_store,
#[cfg(feature = "experimental-encrypted-state-events")]
e2ee.clone(),
&e2ee,
)
.await?;
// This will be used for both invited and knocked rooms.
if let Some((raw_events, events)) = invite_state_events {
if let Some(raw_state_events) = raw_invite_state_events {
state_events::stripped::dispatch_invite_or_knock(
context,
(&raw_events, &events),
raw_state_events,
&room,
&mut room_info,
user_id,
notification::Notification::new(
notification.push_rules,
notification.notifications,
@@ -150,22 +151,10 @@ pub async fn update_any_room(
timeline::builder::Timeline::from(room_response),
notification,
#[cfg(feature = "e2e-encryption")]
e2ee.clone(),
&e2ee,
)
.await?;
// Cache the latest decrypted event in room_info, and also keep any later
// encrypted events, so we can slot them in when we get the keys.
#[cfg(feature = "e2e-encryption")]
cache_latest_events(
&room,
&mut room_info,
&timeline.events,
Some(&context.state_changes),
Some(state_store),
)
.await;
#[cfg(feature = "e2e-encryption")]
e2ee::tracked_users::update_or_set_if_room_is_newly_encrypted(
e2ee.olm_machine,
@@ -218,6 +207,13 @@ pub async fn update_any_room(
Ok(Some((room_info, update)))
}
(RoomState::Invited, None) => {
Ok(Some((room_info, RoomUpdateKind::Invited(InvitedRoom::default()))))
}
(RoomState::Knocked, None) => {
Ok(Some((room_info, RoomUpdateKind::Knocked(KnockedRoom::default()))))
}
_ => Ok(None),
}
}
@@ -229,12 +225,11 @@ pub async fn update_any_room(
/// or knocked room, depending of the membership event (if any).
fn membership(
context: &mut Context,
state_events: &[AnySyncStateEvent],
invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>)>,
state_events: &mut [RawStateEventWithKeys<AnySyncStateEvent>],
invite_state_events: Option<&mut [RawStateEventWithKeys<AnyStrippedStateEvent>]>,
store: &BaseStateStore,
user_id: &UserId,
room_id: &RoomId,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
) -> (Room, RoomInfo, Option<RoomUpdateKind>) {
// There are invite state events. It means the room can be:
//
@@ -245,28 +240,30 @@ fn membership(
if let Some(state_events) = invite_state_events {
// We need to find the membership event since it could be for either an invited
// or knocked room.
let membership_event = state_events.1.iter().find_map(|event| {
if let AnyStrippedStateEvent::RoomMember(membership_event) = event
&& membership_event.state_key == user_id
let own_membership = state_events.iter_mut().find_map(|raw_event| {
if raw_event.event_type == StateEventType::RoomMember
&& raw_event.state_key == user_id.as_str()
{
return Some(membership_event.content.clone());
raw_event
.deserialize_as(|any_event| {
as_variant!(any_event, AnyStrippedStateEvent::RoomMember)
})
.map(|event| event.content.membership.clone())
} else {
None
}
None
});
match membership_event {
let raw_events = state_events.iter().map(|event| event.raw.clone()).collect();
match own_membership {
// There is a membership event indicating it's a knocked room.
Some(RoomMemberEventContent { membership: MembershipState::Knock, .. }) => {
let room = store.get_or_create_room(
room_id,
RoomState::Knocked,
room_info_notable_update_sender,
);
Some(MembershipState::Knock) => {
let room = store.get_or_create_room(room_id, RoomState::Knocked);
let mut room_info = room.clone_info();
// Override the room state if the room already exists.
room_info.mark_as_knocked();
let raw_events = state_events.0.clone();
let knock_state = assign!(KnockState::default(), { events: raw_events });
let knocked_room = assign!(KnockedRoom::default(), { knock_state: knock_state });
@@ -275,16 +272,11 @@ fn membership(
// Otherwise, assume it's an invited room because there are invite state events.
_ => {
let room = store.get_or_create_room(
room_id,
RoomState::Invited,
room_info_notable_update_sender,
);
let room = store.get_or_create_room(room_id, RoomState::Invited);
let mut room_info = room.clone_info();
// Override the room state if the room already exists.
room_info.mark_as_invited();
let raw_events = state_events.0.clone();
let invited_room = InvitedRoom::from(InviteState::from(raw_events));
(room, room_info, Some(RoomUpdateKind::Invited(invited_room)))
@@ -294,61 +286,31 @@ fn membership(
// No invite state events. We assume this is a joined room for the moment. See this block to
// learn more.
else {
let room =
store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
let room = store.get_or_create_room(room_id, RoomState::Joined);
let mut room_info = room.clone_info();
// We default to considering this room joined if it's not an invite. If it's
// actually left (and we remembered to request membership events in
// our sync request), then we can find this out from the events in
// required_state by calling handle_own_room_membership.
// actually left (and we remembered to request membership events in our sync
// request), then we can find this out from the events in required_state by
// calling handle_own_room_membership.
room_info.mark_as_joined();
// We don't need to do this in a v2 sync, because the membership of a room can
// be figured out by whether the room is in the "join", "leave" etc.
// property. In sliding sync we only have invite_state,
// required_state and timeline, so we must process required_state and timeline
// looking for relevant membership events.
own_membership(context, user_id, state_events, &mut room_info);
// be figured out by whether the room is in the `join`, `leave` etc. property.
// In sliding sync we only have `invite_state`, `required_state` and `timeline`,
// so we must process `required_state` and `timeline` looking for relevant
// membership events.
state_events::sync::own_membership_and_update_room_state(
context,
user_id,
state_events,
&mut room_info,
);
(room, room_info, None)
}
}
/// Find any `m.room.member` events that refer to the current user, and update
/// the state in room_info to reflect the "membership" property.
fn own_membership(
context: &mut Context,
user_id: &UserId,
state_events: &[AnySyncStateEvent],
room_info: &mut RoomInfo,
) {
// Start from the last event; the first membership event we see in that order is
// the last in the regular order, so that's the only one we need to
// consider.
for event in state_events.iter().rev() {
if let AnySyncStateEvent::RoomMember(member) = &event {
// If this event updates the current user's membership, record that in the
// room_info.
if member.state_key() == user_id.as_str() {
let new_state: RoomState = member.membership().into();
if new_state != room_info.state() {
room_info.set_state(new_state);
// Update an existing notable update entry or create a new one
context
.room_info_notable_updates
.entry(room_info.room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
break;
}
}
}
}
fn properties(
context: &mut Context,
room_id: &RoomId,
@@ -418,147 +380,6 @@ fn properties(
}
}
/// Find the most recent decrypted event and cache it in the supplied RoomInfo.
///
/// If any encrypted events are found after that one, store them in the RoomInfo
/// too so we can use them when we get the relevant keys.
///
/// It is the responsibility of the caller to update the `RoomInfo` instance
/// stored in the `Room`.
#[cfg(feature = "e2e-encryption")]
pub(crate) async fn cache_latest_events(
room: &Room,
room_info: &mut RoomInfo,
events: &[TimelineEvent],
changes: Option<&StateChanges>,
store: Option<&BaseStateStore>,
) {
use tracing::warn;
use crate::{
deserialized_responses::DisplayName,
latest_event::{LatestEvent, PossibleLatestEvent, is_suitable_for_latest_event},
store::ambiguity_map::is_display_name_ambiguous,
};
let _timer = timer!(tracing::Level::TRACE, "cache_latest_events");
let mut encrypted_events =
Vec::with_capacity(room.latest_encrypted_events.read().unwrap().capacity());
// Try to get room power levels from the current changes. If we didn't get any
// info, try getting it from local data.
let power_levels = match changes.and_then(|changes| changes.power_levels(room_info.room_id())) {
Some(power_levels) => Some(power_levels),
None => room.power_levels().await.ok(),
};
let power_levels_info = Some(room.own_user_id()).zip(power_levels.as_ref());
for event in events.iter().rev() {
if let Ok(timeline_event) = event.raw().deserialize() {
match is_suitable_for_latest_event(&timeline_event, power_levels_info) {
PossibleLatestEvent::YesRoomMessage(_)
| PossibleLatestEvent::YesPoll(_)
| PossibleLatestEvent::YesCallInvite(_)
| PossibleLatestEvent::YesRtcNotification(_)
| PossibleLatestEvent::YesSticker(_)
| PossibleLatestEvent::YesKnockedStateEvent(_) => {
// We found a suitable latest event. Store it.
// In order to make the latest event fast to read, we want to keep the
// associated sender in cache. This is a best-effort to gather enough
// information for creating a user profile as fast as possible. If information
// are missing, let's go back on the “slow” path.
let mut sender_profile = None;
let mut sender_name_is_ambiguous = None;
// First off, look up the sender's profile from the `StateChanges`, they are
// likely to be the most recent information.
if let Some(changes) = changes {
sender_profile = changes
.profiles
.get(room.room_id())
.and_then(|profiles_by_user| {
profiles_by_user.get(timeline_event.sender())
})
.cloned();
if let Some(sender_profile) = sender_profile.as_ref() {
sender_name_is_ambiguous = sender_profile
.as_original()
.and_then(|profile| profile.content.displayname.as_ref())
.and_then(|display_name| {
let display_name = DisplayName::new(display_name);
changes.ambiguity_maps.get(room.room_id()).and_then(
|map_for_room| {
map_for_room.get(&display_name).map(|users| {
is_display_name_ambiguous(&display_name, users)
})
},
)
});
}
}
// Otherwise, look up the sender's profile from the `Store`.
if sender_profile.is_none()
&& let Some(store) = store
{
sender_profile = store
.get_profile(room.room_id(), timeline_event.sender())
.await
.ok()
.flatten();
// TODO: need to update `sender_name_is_ambiguous`,
// but how?
}
let latest_event = Box::new(LatestEvent::new_with_sender_details(
event.clone(),
sender_profile,
sender_name_is_ambiguous,
));
// Store it in the return RoomInfo (it will be saved for us in the room later).
room_info.latest_event = Some(latest_event);
// We don't need any of the older encrypted events because we have a new
// decrypted one.
room.latest_encrypted_events.write().unwrap().clear();
// We can stop looking through the timeline now because everything else is
// older.
break;
}
PossibleLatestEvent::NoEncrypted => {
// m.room.encrypted - this might be the latest event later - we can't tell until
// we are able to decrypt it, so store it for now
//
// Check how many encrypted events we have seen. Only store another if we
// haven't already stored the maximum number.
if encrypted_events.len() < encrypted_events.capacity() {
encrypted_events.push(event.raw().clone());
}
}
_ => {
// Ignore unsuitable events
}
}
} else {
warn!(
event_id = ?event.event_id(),
"Failed to deserialize event as `AnySyncTimelineEvent`",
);
}
}
// Push the encrypted events we found into the Room, in reverse order, so
// the latest is last
room.latest_encrypted_events.write().unwrap().extend(encrypted_events.into_iter().rev());
}
impl State {
/// Construct a [`State`] from the state changes for a joined or left room
/// from a response of the Simplified Sliding Sync endpoint.
@@ -15,12 +15,11 @@
use std::collections::{BTreeMap, BTreeSet};
use ruma::{
OwnedRoomId, OwnedUserId, RoomId,
OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::sync::sync_events::v3::{
InvitedRoom, JoinedRoom, KnockedRoom, LeftRoom, State as RumaState,
},
};
use tokio::sync::broadcast::Sender;
use tracing::error;
#[cfg(feature = "e2e-encryption")]
@@ -30,7 +29,7 @@ use super::{
RoomCreationData,
};
use crate::{
Result, RoomInfoNotableUpdate, RoomState,
Result, RoomState,
sync::{InvitedRoomUpdate, JoinedRoomUpdate, KnockedRoomUpdate, LeftRoomUpdate, State},
};
@@ -42,19 +41,14 @@ pub async fn update_joined_room(
joined_room: JoinedRoom,
updated_members_in_room: &mut BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>>,
notification: notification::Notification<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
) -> Result<JoinedRoomUpdate> {
let RoomCreationData {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
} = room_creation_data;
let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
room_creation_data;
let state_store = notification.state_store;
let room =
state_store.get_or_create_room(room_id, RoomState::Joined, room_info_notable_update_sender);
let room = state_store.get_or_create_room(room_id, RoomState::Joined);
let mut room_info = room.clone_info();
@@ -67,17 +61,17 @@ pub async fn update_joined_room(
let mut new_user_ids = BTreeSet::new();
let state = State::from_sync_v2(joined_room.state);
let (raw_state_events, state_events) = state.collect(&joined_room.timeline.events);
let raw_state_events = state.collect(&joined_room.timeline.events);
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
raw_state_events,
&mut room_info,
ambiguity_cache,
&mut new_user_ids,
state_store,
#[cfg(feature = "experimental-encrypted-state-events")]
e2ee.clone(),
e2ee,
)
.await?;
@@ -153,19 +147,17 @@ pub async fn update_left_room(
room_creation_data: RoomCreationData<'_>,
left_room: LeftRoom,
notification: notification::Notification<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
) -> Result<LeftRoomUpdate> {
let RoomCreationData {
room_id,
room_info_notable_update_sender,
requested_required_states,
ambiguity_cache,
} = room_creation_data;
let RoomCreationData { room_id, requested_required_states, ambiguity_cache } =
room_creation_data;
#[cfg(feature = "e2e-encryption")]
let olm_machine = e2ee.olm_machine;
let state_store = notification.state_store;
let room =
state_store.get_or_create_room(room_id, RoomState::Left, room_info_notable_update_sender);
let room = state_store.get_or_create_room(room_id, RoomState::Left);
let mut room_info = room.clone_info();
room_info.mark_as_left();
@@ -173,17 +165,17 @@ pub async fn update_left_room(
room_info.handle_encryption_state(requested_required_states.for_room(room_id));
let state = State::from_sync_v2(left_room.state);
let (raw_state_events, state_events) = state.collect(&left_room.timeline.events);
let raw_state_events = state.collect(&left_room.timeline.events);
state_events::sync::dispatch(
context,
(&raw_state_events, &state_events),
raw_state_events,
&mut room_info,
ambiguity_cache,
&mut (),
state_store,
#[cfg(feature = "experimental-encrypted-state-events")]
e2ee.clone(),
e2ee,
)
.await?;
@@ -198,6 +190,13 @@ pub async fn update_left_room(
)
.await?;
// Since we are no longer joined to the room, we cannot be waiting for a key
// bundle: clear any flag that we are.
#[cfg(feature = "e2e-encryption")]
if let Some(olm_machine) = olm_machine {
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
}
// Save the new `RoomInfo`.
context.state_changes.add_room(room_info);
@@ -212,19 +211,16 @@ pub async fn update_left_room(
pub async fn update_invited_room(
context: &mut Context,
room_id: &RoomId,
user_id: &UserId,
invited_room: InvitedRoom,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
notification: notification::Notification<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
) -> Result<InvitedRoomUpdate> {
let state_store = notification.state_store;
let room = state_store.get_or_create_room(
room_id,
RoomState::Invited,
room_info_notable_update_sender,
);
let room = state_store.get_or_create_room(room_id, RoomState::Invited);
let (raw_events, events) = state_events::stripped::collect(&invited_room.invite_state.events);
let raw_state_events = state_events::stripped::collect(&invited_room.invite_state.events);
let mut room_info = room.clone_info();
room_info.mark_as_invited();
@@ -232,13 +228,21 @@ pub async fn update_invited_room(
state_events::stripped::dispatch_invite_or_knock(
context,
(&raw_events, &events),
raw_state_events,
&room,
&mut room_info,
user_id,
notification,
)
.await?;
// Since we are no longer joined to the room, we cannot be waiting for a key
// bundle: clear any flag that we are.
#[cfg(feature = "e2e-encryption")]
if let Some(olm_machine) = e2ee.olm_machine {
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
}
context.state_changes.add_room(room_info);
Ok(invited_room)
@@ -248,19 +252,16 @@ pub async fn update_invited_room(
pub async fn update_knocked_room(
context: &mut Context,
room_id: &RoomId,
user_id: &UserId,
knocked_room: KnockedRoom,
room_info_notable_update_sender: Sender<RoomInfoNotableUpdate>,
notification: notification::Notification<'_>,
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'_>,
) -> Result<KnockedRoomUpdate> {
let state_store = notification.state_store;
let room = state_store.get_or_create_room(
room_id,
RoomState::Knocked,
room_info_notable_update_sender,
);
let room = state_store.get_or_create_room(room_id, RoomState::Knocked);
let (raw_events, events) = state_events::stripped::collect(&knocked_room.knock_state.events);
let raw_state_events = state_events::stripped::collect(&knocked_room.knock_state.events);
let mut room_info = room.clone_info();
room_info.mark_as_knocked();
@@ -268,13 +269,21 @@ pub async fn update_knocked_room(
state_events::stripped::dispatch_invite_or_knock(
context,
(&raw_events, &events),
raw_state_events,
&room,
&mut room_info,
user_id,
notification,
)
.await?;
// Since we are not joined to the room, we cannot be waiting for a key
// bundle: clear any flag that we are.
#[cfg(feature = "e2e-encryption")]
if let Some(olm_machine) = e2ee.olm_machine {
olm_machine.store().clear_room_pending_key_bundle(room_info.room_id()).await?
}
context.state_changes.add_room(room_info);
Ok(knocked_room)
@@ -14,40 +14,40 @@
use std::collections::BTreeSet;
use as_variant::as_variant;
use ruma::{
RoomId,
events::{
AnySyncStateEvent, SyncStateEvent,
room::{create::RoomCreateEventContent, tombstone::RoomTombstoneEventContent},
},
events::{AnySyncStateEvent, SyncStateEvent},
serde::Raw,
};
use serde::Deserialize;
use tracing::warn;
use tracing::error;
use super::Context;
use crate::store::BaseStateStore;
#[cfg(feature = "experimental-encrypted-state-events")]
use super::e2ee;
use crate::{store::BaseStateStore, utils::RawStateEventWithKeys};
/// Collect [`AnySyncStateEvent`].
pub mod sync {
use std::{collections::BTreeSet, iter};
use std::collections::BTreeSet;
use as_variant::as_variant;
use ruma::{
OwnedUserId, RoomId, UserId,
events::{
AnySyncTimelineEvent, SyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
AnySyncStateEvent, AnySyncTimelineEvent, StateEventType, room::member::MembershipState,
},
};
use tracing::{error, instrument};
use tracing::instrument;
use super::{super::profiles, AnySyncStateEvent, Context, Raw};
use super::{super::profiles, Context, Raw};
#[cfg(feature = "experimental-encrypted-state-events")]
use crate::response_processors::e2ee;
use crate::{
RoomInfo,
RoomInfo, RoomInfoNotableUpdateReasons, RoomState,
store::{BaseStateStore, Result as StoreResult, ambiguity_map::AmbiguityCache},
sync::State,
utils::RawStateEventWithKeys,
};
impl State {
@@ -58,18 +58,23 @@ pub mod sync {
pub(crate) fn collect(
&self,
timeline: &[Raw<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
) -> Vec<RawStateEventWithKeys<AnySyncStateEvent>> {
match self {
Self::Before(events) => {
super::collect(events.iter().chain(timeline.iter().filter_map(|raw_event| {
// Only state events have a `state_key` field.
match raw_event.get_field::<&str>("state_key") {
Ok(Some(_)) => Some(raw_event.cast_ref_unchecked()),
_ => None,
}
})))
}
Self::After(events) => super::collect(events),
Self::Before(events) => events
.iter()
.cloned()
.filter_map(RawStateEventWithKeys::try_from_raw_state_event)
.chain(
timeline
.iter()
.filter_map(RawStateEventWithKeys::try_from_raw_timeline_event),
)
.collect(),
Self::After(events) => events
.iter()
.cloned()
.filter_map(RawStateEventWithKeys::try_from_raw_state_event)
.collect(),
}
}
}
@@ -86,59 +91,51 @@ pub mod sync {
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub async fn dispatch<U>(
context: &mut Context,
(raw_events, events): (&[Raw<AnySyncStateEvent>], &[AnySyncStateEvent]),
raw_events: Vec<RawStateEventWithKeys<AnySyncStateEvent>>,
room_info: &mut RoomInfo,
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
state_store: &BaseStateStore,
#[cfg(feature = "experimental-encrypted-state-events")] e2ee: e2ee::E2EE<'_>,
#[cfg(feature = "experimental-encrypted-state-events")] e2ee: &e2ee::E2EE<'_>,
) -> StoreResult<()>
where
U: NewUsers,
{
for (raw_event, event) in iter::zip(raw_events, events) {
match event {
AnySyncStateEvent::RoomMember(member) => {
room_info.handle_state_event(event);
for mut raw_event in raw_events {
match (&raw_event.event_type, raw_event.state_key.as_str()) {
(StateEventType::RoomMember, _) => {
room_info.handle_state_event(&mut raw_event);
dispatch_room_member(
context,
&room_info.room_id,
member,
&mut raw_event,
ambiguity_cache,
new_users,
)
.await?;
}
AnySyncStateEvent::RoomCreate(create) => {
let edited_create = super::validate_create_event_predecessor(
(StateEventType::RoomCreate, "") => {
super::validate_create_event_predecessor(
context,
room_info.room_id(),
create,
&room_info.room_id,
&mut raw_event,
state_store,
);
room_info.handle_state_event(
edited_create.map(Into::into).as_ref().unwrap_or(event),
);
room_info.handle_state_event(&mut raw_event);
}
AnySyncStateEvent::RoomTombstone(tombstone) => {
(StateEventType::RoomTombstone, "") => {
if super::is_tombstone_event_valid(
context,
room_info.room_id(),
tombstone,
&room_info.room_id,
&mut raw_event,
state_store,
) {
room_info.handle_state_event(event);
room_info.handle_state_event(&mut raw_event);
} else {
error!(
room_id = ?room_info.room_id(),
?tombstone,
"`m.room.tombstone` event is invalid, it creates a loop"
);
// Do not add the event to `room_info`.
// Do not add the event to `context.state_changes.state`.
continue;
@@ -146,56 +143,18 @@ pub mod sync {
}
#[cfg(feature = "experimental-encrypted-state-events")]
AnySyncStateEvent::RoomEncrypted(SyncStateEvent::Original(outer)) => {
use matrix_sdk_crypto::RoomEventDecryptionResult;
use tracing::{trace, warn};
trace!(event_id = ?outer.event_id, "Received encrypted state event, attempting decryption...");
let Some(olm_machine) = e2ee.olm_machine else {
continue;
};
let decrypted_event = olm_machine
.try_decrypt_room_event(
raw_event.cast_ref_unchecked(),
&room_info.room_id,
e2ee.decryption_settings,
)
.await
.expect("OlmMachine was not started");
// Skip state events that failed to decrypt.
let RoomEventDecryptionResult::Decrypted(decrypted_event) = decrypted_event
(StateEventType::RoomEncrypted, _) => {
let Some(mut raw_event) =
super::decrypt_state_event(&mut raw_event, &room_info.room_id, e2ee).await
else {
warn!(event_id = ?outer.event_id, "Failed to decrypt state event");
continue;
};
// Cast to `AnySyncTimelineEvent`, safe since this is a supertype of
// `AnyTimelineEvent`.
let deserialized_event = match decrypted_event
.event
.deserialize_as::<AnySyncTimelineEvent>()
{
Ok(event) => event,
Err(err) => {
warn!(event_id = ?outer.event_id, "Failed to decrypt state event: {err}");
continue;
}
};
// Ensure decrypted event is actually a state event.
let AnySyncTimelineEvent::State(event) = deserialized_event else {
continue;
};
trace!(event_id = ?outer.event_id, "Decrypted state event successfully.");
room_info.handle_state_event(&event);
room_info.handle_state_event(&mut raw_event);
}
_ => {
room_info.handle_state_event(event);
room_info.handle_state_event(&mut raw_event);
}
}
@@ -204,9 +163,9 @@ pub mod sync {
.state
.entry(room_info.room_id.to_owned())
.or_default()
.entry(event.event_type())
.entry(raw_event.event_type)
.or_default()
.insert(event.state_key().to_owned(), raw_event.clone());
.insert(raw_event.state_key, raw_event.raw);
}
Ok(())
@@ -216,13 +175,19 @@ pub mod sync {
async fn dispatch_room_member<U>(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
) -> StoreResult<()>
where
U: NewUsers,
{
let Some(event) = raw_event
.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomMember))
else {
return Ok(());
};
ambiguity_cache.handle_event(&context.state_changes, room_id, event).await?;
match event.membership() {
@@ -252,26 +217,75 @@ pub mod sync {
impl NewUsers for () {
fn insert(&mut self, _user_id: &UserId) {}
}
/// Find any `m.room.member` events that refer to the current user, and emit
/// a membership update accordingly, plus update the state in `room_info` to
/// reflect the "membership" property.
pub fn own_membership_and_update_room_state(
context: &mut Context,
user_id: &UserId,
state_events: &mut [RawStateEventWithKeys<AnySyncStateEvent>],
room_info: &mut RoomInfo,
) {
// Start from the last event; the first membership event we see in that order is
// the last in the regular order, so that's the only one we need to
// consider.
if let Some(member) = state_events.iter_mut().rev().find_map(|event| {
// Find the event that updates the current user's membership.
if event.event_type == StateEventType::RoomMember
&& event.state_key.as_str() == user_id
&& let Some(member) = event.deserialize_as(|any_event| {
as_variant!(any_event, AnySyncStateEvent::RoomMember)
})
{
Some(member)
} else {
None
}
}) {
let new_state: RoomState = member.membership().into();
if new_state != room_info.state() {
room_info.set_state(new_state);
// Update an existing notable update entry or create a new one
context
.room_info_notable_updates
.entry(room_info.room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
}
}
}
/// Collect [`AnyStrippedStateEvent`].
pub mod stripped {
use std::{collections::BTreeMap, iter};
use std::collections::BTreeMap;
use ruma::{events::AnyStrippedStateEvent, push::Action};
use ruma::{
RoomId, UserId,
events::{AnyStrippedStateEvent, StateEventType},
push::Action,
};
use tracing::instrument;
use super::{
super::{notification, timeline},
Context, Raw,
};
use crate::{Result, Room, RoomInfo};
use crate::{RawStateEventWithKeys, Result, Room, RoomInfo, RoomInfoNotableUpdateReasons};
/// Collect [`Raw<AnyStrippedStateEvent>`] to [`AnyStrippedStateEvent`].
/// Collect [`Raw<AnyStrippedStateEvent>`] to
/// [`RawStateEventWithKeys<AnyStrippedStateEvent>`].
pub fn collect(
raw_events: &[Raw<AnyStrippedStateEvent>],
) -> (Vec<Raw<AnyStrippedStateEvent>>, Vec<AnyStrippedStateEvent>) {
super::collect(raw_events)
) -> Vec<RawStateEventWithKeys<AnyStrippedStateEvent>> {
raw_events
.iter()
.cloned()
.filter_map(RawStateEventWithKeys::try_from_raw_state_event)
.collect()
}
/// Dispatch the stripped state events.
@@ -293,19 +307,23 @@ pub mod stripped {
#[instrument(skip_all, fields(room_id = ?room_info.room_id))]
pub(crate) async fn dispatch_invite_or_knock(
context: &mut Context,
(raw_events, events): (&[Raw<AnyStrippedStateEvent>], &[AnyStrippedStateEvent]),
raw_events: Vec<RawStateEventWithKeys<AnyStrippedStateEvent>>,
room: &Room,
room_info: &mut RoomInfo,
user_id: &UserId,
mut notification: notification::Notification<'_>,
) -> Result<()> {
let mut state_events = BTreeMap::new();
for (raw_event, event) in iter::zip(raw_events, events) {
room_info.handle_stripped_state_event(event);
own_membership(context, room.room_id(), user_id, &raw_events);
for mut raw_event in raw_events {
room_info.handle_stripped_state_event(&mut raw_event);
state_events
.entry(event.event_type())
.entry(raw_event.event_type)
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event.clone());
.insert(raw_event.state_key, raw_event.raw);
}
context
@@ -332,38 +350,52 @@ pub mod stripped {
Ok(())
}
}
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
where
I: IntoIterator<Item = &'a Raw<T>>,
T: Deserialize<'a> + 'a,
{
raw_events
.into_iter()
.filter_map(|raw_event| match raw_event.deserialize() {
Ok(event) => Some((raw_event.clone(), event)),
Err(e) => {
warn!("Couldn't deserialize stripped state event: {e}");
None
}
})
.unzip()
/// Find any `m.room.member` events that refer to the current user, and emit
/// a membership update accordingly.
pub fn own_membership(
context: &mut Context,
room_id: &RoomId,
user_id: &UserId,
raw_state_events: &[RawStateEventWithKeys<AnyStrippedStateEvent>],
) {
// Start from the last event; the first membership event we see in that order is
// the last in the regular order, so that's the only one we need to
// consider.
if raw_state_events.iter().rev().any(|raw_event| {
// Find the event that updates the current user's membership.
raw_event.event_type == StateEventType::RoomMember
&& raw_event.state_key == user_id.as_str()
}) {
// Update an existing notable update entry or create a new one
context
.room_info_notable_updates
.entry(room_id.to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::MEMBERSHIP);
}
}
}
/// Check if the `predecessor` in `m.room.create` isn't creating a loop of
/// rooms.
///
/// If it is, we return a clone of the event with the predecessor removed.
/// If it is, we edit the cached event in `raw_event` to remove the predecessor.
pub fn validate_create_event_predecessor(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomCreateEventContent>,
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
state_store: &BaseStateStore,
) -> Option<SyncStateEvent<RoomCreateEventContent>> {
) {
let mut already_seen = BTreeSet::new();
already_seen.insert(room_id.to_owned());
let Some(event) =
raw_event.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomCreate))
else {
return;
};
// Redacted and non-redacted create events use the same content type.
let content = match event {
SyncStateEvent::Original(event) => &event.content,
@@ -374,7 +406,7 @@ pub fn validate_create_event_predecessor(
content.predecessor.as_ref().map(|predecessor| predecessor.room_id.clone())
else {
// No predecessor = no problem here.
return None;
return;
};
loop {
@@ -391,7 +423,9 @@ pub fn validate_create_event_predecessor(
SyncStateEvent::Redacted(event) => event.content.predecessor.take(),
};
return Some(event);
raw_event.set_cached_event(event.into());
return;
}
already_seen.insert(predecessor_room_id.clone());
@@ -415,32 +449,34 @@ pub fn validate_create_event_predecessor(
predecessor_room_id = next_predecessor_room_id;
}
None
}
/// Check if `m.room.tombstone` isn't creating a loop of rooms.
pub fn is_tombstone_event_valid(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomTombstoneEventContent>,
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
state_store: &BaseStateStore,
) -> bool {
let mut already_seen = BTreeSet::new();
already_seen.insert(room_id.to_owned());
let Some(mut successor_room_id) =
event.as_original().map(|event| event.content.replacement_room.clone())
let Some(tombstone) = raw_event
.deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomTombstone))
.and_then(|event| Some(&event.as_original()?.content))
else {
// `true` means no problem. No successor = no problem here.
return true;
};
let mut successor_room_id = tombstone.replacement_room.clone();
loop {
// We must check immediately if the `successor_room_id` is in `already_seen` in
// case of a room is created and tombstones itself in a single sync.
if already_seen.contains(AsRef::<RoomId>::as_ref(&successor_room_id)) {
// Ahhh, there is a loop with `m.room.tombstone` events!
error!(?room_id, ?tombstone, "`m.room.tombstone` event is invalid, it creates a loop");
return false;
}
@@ -451,7 +487,7 @@ pub fn is_tombstone_event_valid(
.state_changes
.room_infos
.get(&successor_room_id)
.and_then(|room_info| Some(room_info.tombstone()?.replacement_room.clone()))
.and_then(|room_info| room_info.tombstone()?.replacement_room.clone())
.or_else(|| {
state_store
.room(&successor_room_id)
@@ -468,12 +504,71 @@ pub fn is_tombstone_event_valid(
true
}
/// Attempt to decrypt the given state event.
///
/// Returns `Some(_)` if the state event was successfully decrypted and
/// its keys were deserialized.
#[cfg(feature = "experimental-encrypted-state-events")]
async fn decrypt_state_event(
raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
room_id: &RoomId,
e2ee: &e2ee::E2EE<'_>,
) -> Option<RawStateEventWithKeys<AnySyncStateEvent>> {
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::OwnedEventId;
use tracing::{trace, warn};
let event_id = match raw_event.raw.get_field::<OwnedEventId>("event_id") {
Ok(Some(event_id)) => event_id,
Ok(None) => {
warn!("Couldn't deserialize encrypted state event's ID: missing `event_id` field");
return None;
}
Err(error) => {
warn!(?error, "Couldn't deserialize encrypted state event's ID");
return None;
}
};
trace!(?event_id, "Received encrypted state event, attempting decryption...");
let olm_machine = e2ee.olm_machine?;
let decrypted_event = olm_machine
.try_decrypt_room_event(
raw_event.raw.cast_ref_unchecked(),
room_id,
e2ee.decryption_settings,
)
.await
.expect("OlmMachine was not started");
// Skip state events that failed to decrypt.
let RoomEventDecryptionResult::Decrypted(decrypted_event) = decrypted_event else {
warn!(?event_id, "Failed to decrypt state event");
return None;
};
// Cast to `AnySync*Event`, safe since this is a supertype of
// `AnyTimelineEvent`.
match RawStateEventWithKeys::try_from_raw_state_event(decrypted_event.event.cast_unchecked()) {
Some(event) => {
trace!(?event_id, "Decrypted state event successfully.");
Some(event)
}
None => {
warn!(?event_id, "Failed to decrypt state event: decrypted state event is invalid");
None
}
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use matrix_sdk_test::{
DEFAULT_TEST_ROOM_ID, JoinedRoomBuilder, StateTestEvent, SyncResponseBuilder, TestResult,
async_test, event_factory::EventFactory,
DEFAULT_TEST_ROOM_ID, JoinedRoomBuilder, SyncResponseBuilder, TestResult, async_test,
event_factory::EventFactory,
};
use ruma::{RoomVersionId, event_id, room_id, user_id};
@@ -1151,8 +1246,9 @@ mod tests {
let client = logged_in_base_client(Some(user_id)).await;
let mut sync_builder = SyncResponseBuilder::new();
let room_name = EventFactory::new()
.sender(user_id)
let event_factory = EventFactory::new().sender(user_id);
let room_name = event_factory
.room_topic("this is the test topic in the timeline")
.event_id(event_id!("$2"))
.into_raw_sync();
@@ -1161,8 +1257,8 @@ mod tests {
.add_joined_room(
JoinedRoomBuilder::new(&DEFAULT_TEST_ROOM_ID)
.add_timeline_event(room_name)
.add_state_event(StateTestEvent::Create)
.add_state_event(StateTestEvent::PowerLevels),
.add_state_event(event_factory.create(user_id, RoomVersionId::V1))
.add_state_event(event_factory.default_power_levels()),
)
.build_sync_response();
client.receive_sync_response(response).await?;
@@ -43,7 +43,7 @@ pub async fn build<'notification, 'e2ee>(
room_info: &mut RoomInfo,
timeline_inputs: builder::Timeline,
mut notification: notification::Notification<'notification>,
#[cfg(feature = "e2e-encryption")] e2ee: e2ee::E2EE<'e2ee>,
#[cfg(feature = "e2e-encryption")] e2ee: &e2ee::E2EE<'e2ee>,
) -> Result<Timeline> {
let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
@@ -95,7 +95,7 @@ pub async fn build<'notification, 'e2ee>(
) => {
if let Some(decrypted_timeline_event) =
Box::pin(e2ee::decrypt::sync_timeline_event(
e2ee.clone(),
e2ee,
&timeline_event,
room_id,
))
@@ -108,7 +108,7 @@ pub async fn build<'notification, 'e2ee>(
_ => {
Box::pin(verification::process_if_relevant(
&sync_timeline_event,
e2ee.clone(),
e2ee,
room_id,
))
.await?;
@@ -36,7 +36,7 @@ use crate::Result;
/// processing the response with [`OlmMachine::mark_request_as_sent`].
pub async fn process_if_relevant(
event: &AnySyncTimelineEvent,
e2ee: E2EE<'_>,
e2ee: &E2EE<'_>,
room_id: &RoomId,
) -> Result<()> {
if !e2ee.verification_is_allowed {
+179 -12
View File
@@ -12,10 +12,34 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::OwnedUserId;
use ruma::{OwnedUserId, events::rtc::notification::CallIntent};
use super::Room;
/// Represents the consensus state of call intent among room members.
/// Call members can advertise their intent to use audio or video, clients can
/// use this in the UI and also to decide to start camera or not when joining.
///
/// This enum distinguishes between full consensus (all members advertise and
/// agree), partial consensus (only some members advertise, but those who do
/// agree), and no consensus (either no one advertises or advertisers disagree).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CallIntentConsensus {
/// All members advertise and agree
Full(CallIntent),
/// Some members advertise and agree
Partial {
/// The call intent that advertising members agree on.
intent: CallIntent,
/// Number of members advertising and agreeing on this intent.
agreeing_count: u64,
/// Total number of members in the call.
total_count: u64,
},
/// No consensus. No one advertises or advertisers disagree.
None,
}
impl Room {
/// Is there a non expired membership with application `m.call` and scope
/// `m.room` in this room.
@@ -34,6 +58,12 @@ impl Room {
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.info.read().active_room_call_participants()
}
/// Get the consensus call intent for the current call, based on what
/// members are advertising.
pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
self.info.read().active_room_call_consensus_intent()
}
}
#[cfg(test)]
@@ -51,15 +81,20 @@ mod tests {
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
LegacyMembershipDataInit, LivekitFocus,
},
rtc::notification::CallIntent,
},
room_id,
serde::Raw,
time::SystemTime,
user_id,
};
use similar_asserts::assert_eq;
use super::super::{Room, RoomState};
use crate::store::MemoryStore;
use super::{
super::{Room, RoomState},
CallIntentConsensus,
};
use crate::{store::MemoryStore, utils::RawStateEventWithKeys};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
@@ -99,7 +134,7 @@ mod tests {
memberships: Vec<LegacyMembershipData>,
ev_id: &EventId,
user_id: &UserId,
) -> AnySyncStateEvent {
) -> Raw<AnySyncStateEvent> {
let content = CallMemberEventContent::new_legacy(memberships);
EventFactory::new()
.sender(user_id)
@@ -121,11 +156,23 @@ mod tests {
ev_id: &EventId,
user_id: &UserId,
init_data: Option<InitData<'_>>,
) -> AnySyncStateEvent {
let application = Application::Call(CallApplicationContent::new(
) -> Raw<AnySyncStateEvent> {
session_member_state_event_with_intent(ev_id, user_id, init_data, None)
}
fn session_member_state_event_with_intent(
ev_id: &EventId,
user_id: &UserId,
init_data: Option<InitData<'_>>,
call_intent: Option<CallIntent>,
) -> Raw<AnySyncStateEvent> {
let mut app_content = CallApplicationContent::new(
"my_call_id_1".to_owned(),
ruma::events::call::member::CallScope::Room,
));
);
app_content.call_intent = call_intent;
let application = Application::Call(app_content);
let foci_preferred = vec![Focus::Livekit(LivekitFocus::new(
"my_call_foci_alias".to_owned(),
"https://lk.org".to_owned(),
@@ -178,11 +225,14 @@ mod tests {
)
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
fn receive_state_events(room: &Room, events: Vec<Raw<AnySyncStateEvent>>) {
room.info.update_if(|info| {
let mut res = false;
for ev in events {
res |= info.handle_state_event(ev);
res |= info.handle_state_event(
&mut RawStateEventWithKeys::try_from_raw_state_event(ev)
.expect("generated state event should be valid"),
);
}
res
});
@@ -207,7 +257,7 @@ mod tests {
let c_two = legacy_member_state_event(vec![m_init_c1, m_init_c2], event_id!("$123456"), c);
// Intentionally use a non time sorted receive order.
receive_state_events(&room, vec![&c_two, &a_empty, &b_one]);
receive_state_events(&room, vec![c_two, a_empty, b_one]);
room
}
@@ -238,7 +288,7 @@ mod tests {
Some(InitData { device_id: "DEVICE_1".into(), minutes_ago: 20 }),
);
// Intentionally use a non time sorted receive order1
receive_state_events(&room, vec![&m_c1, &m_c2, &a_empty, &b_one]);
receive_state_events(&room, vec![m_c1, m_c2, a_empty, b_one]);
room
}
@@ -272,10 +322,127 @@ mod tests {
let c_empty_membership =
legacy_member_state_event(Vec::new(), event_id!("$12345_1"), &CAROL);
receive_state_events(&room, vec![&b_empty_membership, &c_empty_membership]);
receive_state_events(&room, vec![b_empty_membership, c_empty_membership]);
// We have no active call anymore after emptying the memberships
assert_eq!(Vec::<OwnedUserId>::new(), room.active_room_call_participants());
assert!(!room.has_active_room_call());
}
fn consensus_setup(
alice_intent: Option<CallIntent>,
bob_intent: Option<CallIntent>,
call_intent: Option<CallIntent>,
) -> Vec<Raw<AnySyncStateEvent>> {
let alice_membership = session_member_state_event_with_intent(
event_id!("$1"),
user_id!("@alice:server.name"),
InitData { device_id: device_id!("AAA0"), minutes_ago: 1 }.into(),
alice_intent,
);
let bob_membership = session_member_state_event_with_intent(
event_id!("$1"),
user_id!("@bob:server.name"),
InitData { device_id: device_id!("BAA0"), minutes_ago: 1 }.into(),
bob_intent,
);
let carl_membership = session_member_state_event_with_intent(
event_id!("$2"),
user_id!("@carl:server.name"),
InitData { device_id: device_id!("CAA0"), minutes_ago: 1 }.into(),
call_intent,
);
vec![alice_membership, bob_membership, carl_membership]
}
#[test]
fn test_consensus_intent() {
let test_cases = vec![
// (alice_intent, bob_intent, carl_intent, expected_consensus, description)
(None, None, None, CallIntentConsensus::None, "no intents"),
(
Some(CallIntent::Audio),
None,
None,
CallIntentConsensus::Partial {
intent: CallIntent::Audio,
agreeing_count: 1,
total_count: 3,
},
"one intent 1",
),
(
None,
Some(CallIntent::Audio),
None,
CallIntentConsensus::Partial {
intent: CallIntent::Audio,
agreeing_count: 1,
total_count: 3,
},
"one intent 2",
),
(
None,
None,
Some(CallIntent::Audio),
CallIntentConsensus::Partial {
intent: CallIntent::Audio,
agreeing_count: 1,
total_count: 3,
},
"one intent 3",
),
(
None,
None,
Some(CallIntent::Video),
CallIntentConsensus::Partial {
intent: CallIntent::Video,
agreeing_count: 1,
total_count: 3,
},
"one intent 4",
),
(
None,
Some(CallIntent::Video),
Some(CallIntent::Video),
CallIntentConsensus::Partial {
intent: CallIntent::Video,
agreeing_count: 2,
total_count: 3,
},
"two matching intents",
),
(
Some(CallIntent::Video),
Some(CallIntent::Video),
Some(CallIntent::Video),
CallIntentConsensus::Full(CallIntent::Video),
"all agree",
),
(
Some(CallIntent::Video),
None,
Some(CallIntent::Audio),
CallIntentConsensus::None,
"disagreement",
),
(
Some(CallIntent::Video),
Some(CallIntent::Video),
Some(CallIntent::Audio),
CallIntentConsensus::None,
"disagreement 2",
),
];
for (alice, bob, carl, expected, description) in test_cases {
let (_, room) = make_room_test_helper(RoomState::Joined);
receive_state_events(&room, consensus_setup(alice, bob, carl));
let consensus_intent = room.active_room_call_consensus_intent();
assert_eq!(expected, consensus_intent, "Failed case: {}", description);
}
}
}
+12 -3
View File
@@ -16,7 +16,8 @@ use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
use ruma::{
OwnedUserId, RoomVersionId, assign,
events::{
EmptyStateKey, RedactContent, RedactedStateEventContent, StateEventType,
EmptyStateKey, PossiblyRedactedStateEventContent, RedactContent, RedactedStateEventContent,
StateEventContent, StateEventType, StaticEventContent,
macros::EventContent,
room::create::{PreviousRoom, RoomCreateEventContent},
},
@@ -135,10 +136,10 @@ impl RoomCreateWithCreatorEventContent {
pub type RedactedRoomCreateWithCreatorEventContent = RoomCreateWithCreatorEventContent;
impl RedactedStateEventContent for RedactedRoomCreateWithCreatorEventContent {
type StateKey = EmptyStateKey;
type StateKey = <RoomCreateWithCreatorEventContent as StateEventContent>::StateKey;
fn event_type(&self) -> StateEventType {
StateEventType::RoomCreate
RoomCreateWithCreatorEventContent::TYPE.into()
}
}
@@ -156,3 +157,11 @@ impl RedactContent for RoomCreateWithCreatorEventContent {
fn default_create_room_version_id() -> RoomVersionId {
RoomVersionId::V1
}
impl PossiblyRedactedStateEventContent for RoomCreateWithCreatorEventContent {
type StateKey = <RoomCreateWithCreatorEventContent as StateEventContent>::StateKey;
fn event_type(&self) -> StateEventType {
RoomCreateWithCreatorEventContent::TYPE.into()
}
}
+24 -26
View File
@@ -44,7 +44,7 @@ impl Room {
/// If you need a variant that's sync (but with the drawback that it returns
/// an `Option`), consider using [`Room::cached_display_name`].
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
/// [spec]: <https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room>
pub async fn display_name(&self) -> StoreResult<RoomDisplayName> {
if let Some(name) = self.cached_display_name() {
Ok(name)
@@ -99,7 +99,7 @@ impl Room {
/// or [`Room::display_name`] (async, always returns a value), which should
/// be preferred in general.
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
/// [spec]: <https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room>
pub(crate) async fn compute_display_name(&self) -> StoreResult<UpdatedRoomDisplayName> {
enum DisplayNameOrSummary {
Summary(RoomSummary),
@@ -407,7 +407,7 @@ pub struct RoomHero {
const NUM_HEROES: usize = 5;
/// The name of the room, either from the metadata or calculated
/// according to [matrix specification](https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room)
/// according to [matrix specification](https://spec.matrix.org/latest/client-server-api/#calculating-the-display-name-for-a-room)
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum RoomDisplayName {
/// The room has been named explicitly as
@@ -544,12 +544,14 @@ mod tests {
events::{
StateEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
canonical_alias::{
PossiblyRedactedRoomCanonicalAliasEventContent, RoomCanonicalAliasEventContent,
},
member::{MembershipState, RoomMemberEventContent, StrippedRoomMemberEvent},
name::RoomNameEventContent,
name::{PossiblyRedactedRoomNameEventContent, RoomNameEventContent},
},
},
room_alias_id, room_id,
owned_room_alias_id, owned_user_id, room_alias_id, room_id,
serde::Raw,
user_id,
};
@@ -557,8 +559,7 @@ mod tests {
use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
use crate::{
MinimalStateEvent, OriginalMinimalStateEvent, RoomHero, RoomState, StateChanges,
StateStore, store::MemoryStore,
MinimalStateEvent, RoomHero, RoomState, StateChanges, StateStore, store::MemoryStore,
};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
@@ -584,22 +585,22 @@ mod tests {
}
fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
MinimalStateEvent::Original(OriginalMinimalStateEvent {
content: assign!(RoomCanonicalAliasEventContent::new(), {
alias: Some(room_alias_id!("#test:example.com").to_owned()),
MinimalStateEvent {
content: assign!(PossiblyRedactedRoomCanonicalAliasEventContent::new(), {
alias: Some(owned_room_alias_id!("#test:example.com")),
}),
event_id: None,
})
}
}
fn make_name_event_with(name: &str) -> MinimalStateEvent<RoomNameEventContent> {
MinimalStateEvent::Original(OriginalMinimalStateEvent {
content: RoomNameEventContent::new(name.to_owned()),
fn make_name_event_with(name: &str) -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
MinimalStateEvent {
content: RoomNameEventContent::new(name.to_owned()).into(),
event_id: None,
})
}
}
fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
fn make_name_event() -> MinimalStateEvent<PossiblyRedactedRoomNameEventContent> {
make_name_event_with("Test Room")
}
@@ -697,10 +698,7 @@ mod tests {
async fn test_display_name_for_invited_room_is_empty_if_room_name_empty() {
let (_, room) = make_room_test_helper(RoomState::Invited);
let room_name = MinimalStateEvent::Original(OriginalMinimalStateEvent {
content: RoomNameEventContent::new(String::new()),
event_id: None,
});
let room_name = make_name_event_with("");
room.info.update(|info| info.base_info.name = Some(room_name));
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
@@ -1037,27 +1035,27 @@ mod tests {
None,
vec![
RoomHero {
user_id: user_id!("@alice:example.org").to_owned(),
user_id: owned_user_id!("@alice:example.org"),
display_name: Some("Alice".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@bob:example.org").to_owned(),
user_id: owned_user_id!("@bob:example.org"),
display_name: Some("Bob".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@carol:example.org").to_owned(),
user_id: owned_user_id!("@carol:example.org"),
display_name: Some("Carol".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@denis:example.org").to_owned(),
user_id: owned_user_id!("@denis:example.org"),
display_name: Some("Denis".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@erica:example.org").to_owned(),
user_id: owned_user_id!("@erica:example.org"),
display_name: Some("Erica".to_owned()),
avatar_url: None,
},
+10 -6
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use ruma::events::room::encryption::RoomEncryptionEventContent;
use ruma::events::room::encryption::PossiblyRedactedRoomEncryptionEventContent;
use super::Room;
@@ -24,7 +24,7 @@ impl Room {
/// Get the `m.room.encryption` content that enabled end to end encryption
/// in the room.
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
pub fn encryption_settings(&self) -> Option<PossiblyRedactedRoomEncryptionEventContent> {
self.info.read().base_info.encryption.clone()
}
}
@@ -90,12 +90,13 @@ mod tests {
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, event_id,
events::{AnySyncStateEvent, room::encryption::RoomEncryptionEventContent},
room_id,
serde::Raw,
time::SystemTime,
user_id,
};
use super::{EncryptionState, Room};
use crate::{RoomState, store::MemoryStore};
use crate::{RoomState, store::MemoryStore, utils::RawStateEventWithKeys};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
@@ -113,11 +114,14 @@ mod tests {
.expect("date out of range")
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
fn receive_state_events(room: &Room, events: Vec<Raw<AnySyncStateEvent>>) {
room.info.update_if(|info| {
let mut res = false;
for ev in events {
res |= info.handle_state_event(ev);
res |= info.handle_state_event(
&mut RawStateEventWithKeys::try_from_raw_state_event(ev)
.expect("generated state event should be valid"),
);
}
res
});
@@ -140,7 +144,7 @@ mod tests {
// in the roomInfo
.server_ts(timestamp(0))
.into();
receive_state_events(&room, vec![&encryption_event]);
receive_state_events(&room, vec![encryption_event]);
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
}
+9 -279
View File
@@ -12,297 +12,27 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(feature = "e2e-encryption")]
use std::{collections::BTreeMap, num::NonZeroUsize};
use ruma::MilliSecondsSinceUnixEpoch;
#[cfg(feature = "e2e-encryption")]
use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
use super::Room;
#[cfg(feature = "e2e-encryption")]
use super::RoomInfoNotableUpdateReasons;
use crate::latest_event::{LatestEvent, LatestEventValue};
use crate::latest_event::LatestEventValue;
impl Room {
/// The size of the latest_encrypted_events RingBuffer
#[cfg(feature = "e2e-encryption")]
pub(super) const MAX_ENCRYPTED_EVENTS: NonZeroUsize = NonZeroUsize::new(10).unwrap();
/// Return the last event in this room, if one has been cached during
/// sliding sync.
pub fn latest_event(&self) -> Option<LatestEvent> {
self.info.read().latest_event.as_deref().cloned()
}
/// Return the [`LatestEventValue`] of this room.
///
/// Note that it clones the [`LatestEventValue`]! This can be add pressure
/// Note that it clones the [`LatestEventValue`]! This can add pressure
/// on the memory if used in a hot path.
pub fn new_latest_event(&self) -> LatestEventValue {
self.info.read().new_latest_event.clone()
pub fn latest_event(&self) -> LatestEventValue {
self.info.read().latest_event_value.clone()
}
/// Return the value of [`LatestEventValue::timestamp`].
pub fn new_latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
self.info.read().new_latest_event.timestamp()
pub fn latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
self.info.read().latest_event_value.timestamp()
}
/// Return the value of [`LatestEventValue::is_local`].
pub fn new_latest_event_is_local(&self) -> bool {
self.info.read().new_latest_event.is_local()
}
/// Return the most recent few encrypted events. When the keys come through
/// to decrypt these, the most recent relevant one will replace
/// latest_event. (We can't tell which one is relevant until
/// they are decrypted.)
#[cfg(feature = "e2e-encryption")]
pub(crate) fn latest_encrypted_events(&self) -> Vec<Raw<AnySyncTimelineEvent>> {
self.latest_encrypted_events.read().unwrap().iter().cloned().collect()
}
/// Replace our latest_event with the supplied event, and delete it and all
/// older encrypted events from latest_encrypted_events, given that the
/// new event was at the supplied index in the latest_encrypted_events
/// list.
///
/// Panics if index is not a valid index in the latest_encrypted_events
/// list.
///
/// It is the responsibility of the caller to apply the changes into the
/// state store after calling this function.
#[cfg(feature = "e2e-encryption")]
pub(crate) fn on_latest_event_decrypted(
&self,
latest_event: Box<LatestEvent>,
index: usize,
changes: &mut crate::StateChanges,
room_info_notable_updates: &mut BTreeMap<OwnedRoomId, RoomInfoNotableUpdateReasons>,
) {
self.latest_encrypted_events.write().unwrap().drain(0..=index);
let room_info = changes
.room_infos
.entry(self.room_id().to_owned())
.or_insert_with(|| self.clone_info());
room_info.latest_event = Some(latest_event);
room_info_notable_updates
.entry(self.room_id().to_owned())
.or_default()
.insert(RoomInfoNotableUpdateReasons::LATEST_EVENT);
}
}
#[cfg(all(test, feature = "e2e-encryption"))]
mod tests_with_e2e_encryption {
use std::sync::Arc;
use assert_matches::assert_matches;
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_test::async_test;
use ruma::{room_id, serde::Raw, user_id};
use serde_json::json;
use crate::{
BaseClient, Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
SessionMeta, StateChanges,
client::ThreadingSupport,
latest_event::LatestEvent,
response_processors as processors,
store::{MemoryStore, RoomLoadSettings, StoreConfig},
};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
let user_id = user_id!("@me:example.org");
let room_id = room_id!("!test:localhost");
let (sender, _receiver) = tokio::sync::broadcast::channel(1);
(store.clone(), Room::new(user_id, store, room_id, room_type, sender))
}
#[async_test]
async fn test_setting_the_latest_event_doesnt_cause_a_room_info_notable_update() {
// Given a room,
let client = BaseClient::new(
StoreConfig::new("cross-process-store-locks-holder-name".to_owned()),
ThreadingSupport::Disabled,
);
client
.activate(
SessionMeta {
user_id: user_id!("@alice:example.org").into(),
device_id: ruma::device_id!("AYEAYEAYE").into(),
},
RoomLoadSettings::default(),
None,
)
.await
.unwrap();
let room_id = room_id!("!test:localhost");
let room = client.get_or_create_room(room_id, RoomState::Joined);
// That has an encrypted event,
add_encrypted_event(&room, "$A");
// Sanity: it has no latest_event
assert!(room.latest_event().is_none());
// When I set up an observer on the latest_event,
let mut room_info_notable_update = client.room_info_notable_update_receiver();
// And I provide a decrypted event to replace the encrypted one,
let event = make_latest_event("$A");
let mut context = processors::Context::default();
room.on_latest_event_decrypted(
event.clone(),
0,
&mut context.state_changes,
&mut context.room_info_notable_updates,
);
assert!(context.room_info_notable_updates.contains_key(room_id));
// The subscriber isn't notified at this point.
assert!(room_info_notable_update.is_empty());
// Then updating the room info will store the event,
processors::changes::save_and_apply(
context,
&client.state_store,
&client.ignore_user_list_changes,
None,
)
.await
.unwrap();
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
// And wake up the subscriber.
assert_matches!(
room_info_notable_update.recv().await,
Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons }) => {
assert_eq!(received_room_id, room_id);
assert!(reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT));
}
);
}
#[async_test]
async fn test_when_we_provide_a_newly_decrypted_event_it_replaces_latest_event() {
use std::collections::BTreeMap;
// Given a room with an encrypted event
let (_store, room) = make_room_test_helper(RoomState::Joined);
add_encrypted_event(&room, "$A");
// Sanity: it has no latest_event
assert!(room.latest_event().is_none());
// When I provide a decrypted event to replace the encrypted one
let event = make_latest_event("$A");
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
room.on_latest_event_decrypted(
event.clone(),
0,
&mut changes,
&mut room_info_notable_updates,
);
room.set_room_info(
changes.room_infos.get(room.room_id()).cloned().unwrap(),
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
);
// Then is it stored
assert_eq!(room.latest_event().unwrap().event_id(), event.event_id());
}
#[cfg(feature = "e2e-encryption")]
#[async_test]
async fn test_when_a_newly_decrypted_event_appears_we_delete_all_older_encrypted_events() {
// Given a room with some encrypted events and a latest event
use std::collections::BTreeMap;
let (_store, room) = make_room_test_helper(RoomState::Joined);
room.info.update(|info| info.latest_event = Some(make_latest_event("$A")));
add_encrypted_event(&room, "$0");
add_encrypted_event(&room, "$1");
add_encrypted_event(&room, "$2");
add_encrypted_event(&room, "$3");
// When I provide a latest event
let new_event = make_latest_event("$1");
let new_event_index = 1;
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
room.on_latest_event_decrypted(
new_event.clone(),
new_event_index,
&mut changes,
&mut room_info_notable_updates,
);
room.set_room_info(
changes.room_infos.get(room.room_id()).cloned().unwrap(),
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
);
// Then the encrypted events list is shortened to only newer events
let enc_evs = room.latest_encrypted_events();
assert_eq!(enc_evs.len(), 2);
assert_eq!(enc_evs[0].get_field::<&str>("event_id").unwrap().unwrap(), "$2");
assert_eq!(enc_evs[1].get_field::<&str>("event_id").unwrap().unwrap(), "$3");
// And the event is stored
assert_eq!(room.latest_event().unwrap().event_id(), new_event.event_id());
}
#[async_test]
async fn test_replacing_the_newest_event_leaves_none_left() {
use std::collections::BTreeMap;
// Given a room with some encrypted events
let (_store, room) = make_room_test_helper(RoomState::Joined);
add_encrypted_event(&room, "$0");
add_encrypted_event(&room, "$1");
add_encrypted_event(&room, "$2");
add_encrypted_event(&room, "$3");
// When I provide a latest event and say it was the very latest
let new_event = make_latest_event("$3");
let new_event_index = 3;
let mut changes = StateChanges::default();
let mut room_info_notable_updates = BTreeMap::new();
room.on_latest_event_decrypted(
new_event,
new_event_index,
&mut changes,
&mut room_info_notable_updates,
);
room.set_room_info(
changes.room_infos.get(room.room_id()).cloned().unwrap(),
room_info_notable_updates.get(room.room_id()).copied().unwrap(),
);
// Then the encrypted events list ie empty
let enc_evs = room.latest_encrypted_events();
assert_eq!(enc_evs.len(), 0);
}
fn add_encrypted_event(room: &Room, event_id: &str) {
room.latest_encrypted_events
.write()
.unwrap()
.push(Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap());
}
fn make_latest_event(event_id: &str) -> Box<LatestEvent> {
Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
Raw::from_json_string(json!({ "event_id": event_id }).to_string()).unwrap(),
)))
/// Return the value of [`LatestEventValue::is_unsent`].
pub fn latest_event_is_unsent(&self) -> bool {
self.info.read().latest_event_value.is_unsent()
}
}
+4 -4
View File
@@ -259,9 +259,9 @@ impl RoomMember {
/// Get the display name of the member if there is one.
pub fn display_name(&self) -> Option<&str> {
if let Some(p) = self.profile.as_ref() {
p.as_original().and_then(|e| e.content.displayname.as_deref())
p.content.displayname.as_deref()
} else {
self.event.original_content()?.displayname.as_deref()
self.event.displayname_value()
}
}
@@ -276,9 +276,9 @@ impl RoomMember {
/// Get the avatar url of the member, if there is one.
pub fn avatar_url(&self) -> Option<&MxcUri> {
if let Some(p) = self.profile.as_ref() {
p.as_original().and_then(|e| e.content.avatar_url.as_deref())
p.content.avatar_url.as_deref()
} else {
self.event.original_content()?.avatar_url.as_deref()
self.event.avatar_url()
}
}

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