Compare commits

...

1185 Commits

Author SHA1 Message Date
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
Damir Jelić 742a0db07b chore: Remove mentions of the 0.15 release
The 0.15.0 release was a misfire so we're skipping this version number.
2025-12-04 09:59:04 +01:00
Damir Jelić 4701faf039 chore: Release matrix-sdk version 0.16.0 2025-12-04 09:59:04 +01:00
Doug 93f71ba977 ffi: Add support for checking login with QR code availability. 2025-12-03 18:35:29 +02:00
Damir Jelić 4ea0418abe fix: Don't attempt to serialize custom join rules (#5924)
This is not supported by Ruma. The join_rule field, despite being
defined as a pure string, can have associated data to it based on the
join rule variant.

This means that custom and unknown enum variants might lose data when
reserializing.

Let's just skip the serialization of custom join rules in the RoomInfo,
the concrete value is still available in the state store, it's just not
kept at hand in the RoomInfo.

Signed-off-by: Damir Jelić <poljar@termina.org.uk>
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
2025-12-03 16:54:58 +01:00
Jorge Martín 2412403a1e doc: Add changelogs 2025-12-03 15:41:42 +01:00
Jorge Martín ed72d3439a fix: avoid unwrap in Client::optimize_stores 2025-12-03 15:41:42 +01:00
Jorge Martín 9eee20b4f0 feat(ffi): add bindings for Client::get_store_sizes 2025-12-03 15:41:42 +01:00
Jorge Martín bc45457d0e feat: add Client::get_store_sizes
This method will retrieve the database sizes if available and expose it in the client.

Note: the actual database size measuring is only implemented for the SQLite based stores
2025-12-03 15:41:42 +01:00
Jorge Martín 76651aec69 refactor: don't use full namespace for std::Result 2025-12-03 15:41:42 +01:00
Jorge Martín e1cda064ee refactor: hopefully fix another lint error 2025-12-03 15:41:42 +01:00
Jorge Martín 94e5dbea0c refactor: hide optimize_store methods, add warnings to not use them in production
Also fix lint issue
2025-12-03 15:41:42 +01:00
Jorge Martín c6e7a17f65 feat(ffi) Add Client::optimize_stores method 2025-12-03 15:41:42 +01:00
Jorge Martín 1e7bc1286e refactor: Add trace log to ensure the VACUUM operation has finished successfully
This was a bit confusing, because I treated a lack of logs as success when in reality my code was calling an empty implementation
2025-12-03 15:41:42 +01:00
Jorge Martín b04cc9fe27 feat: Implement the new Store::optimize method added in the store traits
Only SQLite based stores will implement it for now, calling the `SqliteAsyncConnExt::vacuum` method
2025-12-03 15:41:42 +01:00
Jorge Martín 054dc31ce4 feat(sdk): Add Client::optimize_stores
This method should trigger any optimization/maintenance behaviours available to the stores, like `VACUUM` in SQLite
2025-12-03 15:41:42 +01:00
Ivan Enderlin aaff9c5d72 test: Update tests according to last patches. 2025-12-03 13:11:40 +01:00
Ivan Enderlin b1773d33c2 fix(sqlite): Make it possible to store the new SendRequestKey format. 2025-12-03 13:11:40 +01:00
Ivan Enderlin 090351c6ac doc(sqlite): Fix mention of a method.
This patch fixes a mention to a `save_send_queue_event` method. It
doesn't exist: it's `save_send_queue_request`.
2025-12-03 13:11:40 +01:00
Ivan Enderlin 045eb3486b test: Use SerializableEventContent::new instead of from_raw.
This patch replaces calls to `SerializableEventContent::from_raw` by
`new`: it's simpler and safer as it's not possible to use an invalid
event type.
2025-12-03 13:11:40 +01:00
Ivan Enderlin 40738ae119 feat(sdk): The Send Queue stores the sent event in the Event Cache.
The event has been sent to the server and the server has received it.
Yepee! Now, we usually wait on the server to give us back the event via
the sync.

Problem: sometimes the network lags, can be down, or the server may be
slow; well, anything can happen. It results in a weird situation where
the user sees its event being sent, then disappears before it's received
again from the server.

To avoid this situation, this patch eagerly saves the event in the Event
Cache. It's similar to what would happen if the event was echoed back
from the server via the sync, but we avoid any network issues. The Event
Cache is smart enought to deduplicate events based on the event ID, so
it's safe to do that.
2025-12-03 13:11:40 +01:00
Doug acc66266c7 fix: Don't show a syncing indicator until the sync service is started. 2025-12-03 12:04:31 +01:00
Ivan Enderlin e6094e6b07 chore: Use our own fork of indexed-db-futures.
This patch uses our own fork of `indexed-db-futures`: `matrix-indexed-db-futures`.
2025-12-03 11:59:50 +01:00
Johannes Marbach ea538351e9 docs(timeline): clarify what mark_as_read actually does
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-12-03 11:58:16 +01:00
Ivan Enderlin b4d7881a58 chore: Reduce the number of logs.
This patch removes some logs around the cross-process lock methods. This
is called pretty often by the cross-process lock task, which pollute the
log files.
2025-12-02 21:59:46 +01:00
Stefan Ceriu 8c4a19bb85 fix(ffi): remove undesired network request from the client builder
Making network requests before actually building a client interferes with offline support, especially so in lie-fi situations.
The method is exposed through FFI though and can be used at the final user's discretion (e.g. when submitting a bug report).
2025-12-02 18:37:10 +01:00
Doug 017644864a chore: Add tests for message-like read receipt tracking. 2025-12-02 15:36:34 +01:00
Doug 59604713e8 chore: Re-use the existing track_read_receipts setting to hide receipts on state events.
# Conflicts:
#	bindings/matrix-sdk-ffi/CHANGELOG.md
2025-12-02 15:36:34 +01:00
Doug d563cebcfc feat: Allow Timelines to be configured to hide read receipts on state events. 2025-12-02 15:36:34 +01:00
Ivan Enderlin 19b7036119 doc: Fix typos. 2025-12-02 11:54:31 +01:00
dependabot[bot] d6b942d3ac chore(deps): bump crate-ci/typos from 1.39.2 to 1.40.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.39.2 to 1.40.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.39.2...v1.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 11:54:31 +01:00
Kévin Commaille c1302c417a feat(sdk): Allow to refresh the token in Client::fetch_server_versions
We need to handle 2 possible deadlocks for this:

1. We cannot try to refresh an expired access token if this call happens
   while we are currently trying to refresh the token. The easiest way
   to handle this is to never try to refresh the token when making this
   call inside `get_path_builder_input()` so we implement a "failsafe"
   mode that disables refreshing the access token in case it expired.
   However it attempts the GET /versions again without the token.
2. We cannot access the cached supported versions if we are in the
   process of refreshing that cache because the RwLock has a write lock.
   So if the access token has expired and we try to refresh it, the
   possible calls to `get_path_builder_input()` must not wait for a read
   lock to be available. So the solution is to never wait for a read
   lock, and skip the cache if a read lock is not available.

This also gets rid of workarounds in other functions.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-02 10:51:49 +00:00
Kévin Commaille 176684a07c feat(sdk): Keep track of whether the access token is expired
This will allow to handle automatically whether to send an access token
or not on endpoints that don't require it in contexts were can't refresh
it.

We also don't cache calls to GET /versions that were not authenticated,
because they might lack some features compared to an authenticated
request.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-02 10:51:49 +00:00
Kévin Commaille be9e7ac9bf test(sdk): Handle more cases with ExpectedAccessToken
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-02 10:51:49 +00:00
Kévin Commaille 407621f055 refactor(sdk): Add constructor for AuthCtx
Allows to have stricter visibility for the fields and put less code in
ClientBuilder.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-02 10:51:49 +00:00
Kévin Commaille bbc6df78ae refactor(sdk): Put ruma-federation-api dependency behind a feature
In theory clients shouldn't make requests to the server-server API. A
way to work around it for this specific case would be to implement
MSC4383.

In the meantime, clients that don't want to use
`Client::server_vendor_info()` won't have to build the extra
dependencies added by ruma-federation-api.

The feature is enabled for the bindings, so it isn't a breaking change
for matrix-sdk-ffi.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-02 11:04:49 +01:00
Mauro 9f02dcd412 ffi(bindings): added is_space to the NotificationRoomInfo (#5907)
Exposes the `is_space` flag to FFI in the `NotificationRoomInfo`, so
that a client can tell through a notification if the room that generated
it, is a space or not.
2025-12-02 09:31:53 +00:00
Ivan Enderlin ab98028a2e feat(sdk): An edit can be a LatestEventValue if it targets the immediate previous event.
This patch changes the rule of what is a `LatestEventValue` candidate
in case of an edit. An edit must target/relate to its immediate previous
event to be a candidate. Otherwise it's easy to edit an old message
and create a “broken” `LatestEventValue` because it points to an older
message that the user may not be able to find easily.
2025-12-01 16:28:24 +01:00
Ivan Enderlin 7c7cbb2566 feat(sdk,ui): Support edits as LatestEventValue.
This patch supports any edits at a possible `LatestEventValue`
candidate.
2025-12-01 16:28:24 +01:00
Ivan Enderlin 32b4bbc1b0 test(ui): Use the EventFactory. 2025-12-01 16:28:24 +01:00
Kévin Commaille e47867f232 refactor(sdk): Split supported versions and well-known cache
The supported versions are necessary for querying almost all endpoints,
but after homeserver auto-discovery the well-known info is only
necessary to get the MatrixRTC foci advertised by the homeserver. So it
shouldn't be necessary to always request both at the same time.

Besides:

- Not all clients support MatrixRTC, so they don't need the well-known
  info.
- The well-known info is only supposed to be used for homeserver
  auto-discovery before login. In fact, the MatrixRTC MSC was changed to
  use a new endpoint for this.
- We don't have access to the server name after restoring the Client, so
  the well-known lookup is more likely to fail.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-01 15:22:48 +00:00
Kévin Commaille 4411274b12 refactor(base): Split TTL store logic from ServerInfo into new type
To make it reusable.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-01 15:22:48 +00:00
Kévin Commaille 32b72580da Commit changed Cargo.lock
This seems to come from a previous commit?

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-12-01 15:22:48 +00:00
Ivan Enderlin 73449f4f57 doc(sdk): Add #5908 in the CHANGELOG.md. 2025-11-27 17:23:47 +01:00
Ivan Enderlin bbe35e8190 fix(sdk): A new local LatestEventValue can be “cannot be sent”.
This patch fixes a bug where a new local `LatestEventValue`
was always created as `LocalIsSending`. It must be created as
`LocalCannotBeSent` if a previous local `LatestEventValue` exists and is
`LocalCannotBeSent`.

This patch adds the companion test too.
2025-11-27 17:23:47 +01:00
Marcel-Nordeck 107fc07d08 Wasm improvements for the bindings
This patch improves the Wasm support of the matrix-sdk-ffi crate.

First a uniffi feature needed to be enabled.
Secondly a bunch of methods which don't work under Wasm have been stubbed out.

Signed-off-by: MTRNord <MTRNord@users.noreply.github.com>
Co-authored-by: MTRNord <MTRNord@users.noreply.github.com>
2025-11-27 15:46:33 +01:00
Damir Jelić 4585d5f4d8 docs(search): Remove the example depending on the matrix-sdk crate
Examples are great, but the circular dependency this introduces is not
worth the trouble.
2025-11-27 14:34:35 +01:00
Damir Jelić 239203a813 fix: The search crate doesn't actually depend on the main crate
This removes a circular dependency we had resulting in a semi-broken
release process.
2025-11-27 14:34:35 +01:00
Damir Jelić 24d7518a01 chore: Allow release branches for cargo release as well 2025-11-27 11:10:32 +01:00
Damir Jelić e6059251d0 Merge pull request #5901 from matrix-org/poljar/release-0.15.0 2025-11-27 10:39:45 +01:00
Damir Jelić f4fef6e995 chore: Fix the dates of the 0.15.0 release 2025-11-27 10:07:41 +01:00
Damir Jelić 850b7dde6d chore: Release matrix-sdk version 0.15.0 2025-11-26 15:44:26 +01:00
Damir Jelić 700c17f383 release: Allow release preparation to work on the HEAD
This is to allow Jujutsu users to use the cargo-release tooling.
2025-11-26 15:38:29 +01:00
Doug 9e842a5d07 spaces: Add support for getting a flattened list of editable spaces. 2025-11-26 13:06:49 +01:00
Doug 18175c1cd0 chore: Use a common add_space_rooms function for tests. 2025-11-26 13:06:49 +01:00
dependabot[bot] 100a04ae2c chore(deps): bump CodSpeedHQ/action from 4.3.3 to 4.4.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.3.3 to 4.4.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/bb005fe1c1eea036d3894f02c049cb6b154a1c27...346a2d8a8d9d38909abd0bc3d23f773110f076ad)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 10:30:10 +01:00
dependabot[bot] 3a655083d6 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 04b9adbd8c1c00963289b5628510dd907b27dc60 to 10aef304cba9ef99dacee57a756c14892391cdca.
- [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/04b9adbd8c1c00963289b5628510dd907b27dc60...10aef304cba9ef99dacee57a756c14892391cdca)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 09:51:58 +01:00
dependabot[bot] 46947be662 chore(deps): bump crate-ci/typos from 1.39.0 to 1.39.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.39.0 to 1.39.2.
- [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.39.0...v1.39.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-26 09:51:05 +01:00
Johannes Marbach fccafd8c80 feat(oauth): expose session expiration errors when requesting login with a QR code
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-26 09:50:11 +01:00
Johannes Marbach 1e30d5f0b0 feat(oauth): expose session expiration errors when granting login with a QR code
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-26 09:50:11 +01:00
Johannes Marbach 4ab12543ce feat(testing): allow specifying expiration duration in MockedRendezvousServer
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-26 09:50:11 +01:00
Johannes Marbach a82ccf1069 fix(oauth): expose client API errors when receiving messages on rendezvous channel
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-26 09:50:11 +01:00
Ivan Enderlin 45a9d96573 chore(sdk): Remove an unwrap in debug_string. 2025-11-25 15:41:27 +01:00
Ivan Enderlin c9324b2f30 refactor(sdk): Change a Semaphore(permit=1) for a Mutex.
This patch changes the `Semaphore(permit=1)` for a `Mutex`: the
semantics is strictly equivalent, but it removes the need to guarantee
there is a single permit.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 8e93bb5373 chore: Replace unwrap by expect. 2025-11-25 15:41:27 +01:00
Ivan Enderlin 8df55fa3e7 test(sdk): Add a test for dirtiness handling in RoomEventCacheStateLock::new. 2025-11-25 15:41:27 +01:00
Ivan Enderlin 478df4af33 test(sdk): Ensure EventCacheStoreLockGuard::clear_dirty is called!
This patch ensures that the `EventCacheStoreLockGuard::clear_dirty`
method is correctly called.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 04fdf7f2f6 feat(sdk): Send updates when RoomEventCacheStateLock is reloaded.
This patch updates the reloading of `RoomEventCacheStateLock`
when the cross-process lock over the store is dirty to broadcast
`RoomEventCacheUpdate` and `RoomEventCacheGenericUpdate`. That way the
`Timeline` and other components can react to this reload.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 179136a9a4 refactor(sdk): Rename RoomEventCacheInner::sender to update_sender.
This patch renames the `sender` field of `RoomEventCacheInner` to
`update_sender` to clarify what the sender is about.
2025-11-25 15:41:27 +01:00
Ivan Enderlin e51996a47c test(sdk): Add the test_reset_when_dirty test.
This patch adds the new `test_reset_when_dirty` test, which ensures
the state is correctly reset when the cross-process lock over the store
becomes dirty.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 12e5614fc8 feat(sdk): Allow shared access on RoomEventCacheStateLock::read.
This patch fixes a problem found in a test (not commited yet) where it
was impossible to do multiple calls to `read` if the first guard was
still alive. See the comments to learn more.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 14d550739a feat(sdk): Implement RoomEventCacheStateLockWriteGuard::downgrade.
This patch implements `RoomEventCacheStateLockWriteGuard::downgrade` to
simplify the code a little bit.
2025-11-25 15:41:27 +01:00
Ivan Enderlin fbcd8ef546 test(common): Make tests run faster.
This patch replaces `sleep` by `yield_now`, which has the same effect in
this case, and makes the tests run faster.
2025-11-25 15:41:27 +01:00
Ivan Enderlin e5f6153f54 test(common): Test dirtiness of the cross-process lock. 2025-11-25 15:41:27 +01:00
Ivan Enderlin badba6eebc fix(sdk): Remove a warning for wasm32. 2025-11-25 15:41:27 +01:00
Ivan Enderlin 72f2296809 doc(sdk): Add missing or fix documentation. 2025-11-25 15:41:27 +01:00
Ivan Enderlin b1af16ef09 feat(sdk): Reset RoomEventCacheState when the cross-process lock is dirty.
This patch updates the `RoomEventCacheStateLock::read` and `write`
methods to reset the state when the cross-process lock is dirty.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 1cf0601ba3 refactor(sdk) Introduce RoomEventCacheStateLock and read/write guards.
This patch extracts fields from `RoomEventCacheState` and move them
into `RoomEventCacheStateLock`. This lock provides 2 methods: `read`
and `write`, respectively to acquire a read-only lock, and a write-only
lock, represented by the `RoomEventCacheStateLockReadGuard` and the
`RoomEventCacheStateLockWriteGuard` types.

All “public” methods on `RoomEventCacheState` now are facade to the read
and write guards.

This refactoring makes the code to compile with the last change in
`EventCacheStore::lock`, which now returns a `EventCacheStoreLockState`.
The next step is to re-load `RoomEventCacheStateLock` when the lock is
dirty! But before doing that, we need this new mechanism to centralise
the management of the store lock.
2025-11-25 15:41:27 +01:00
Ivan Enderlin e034a51b7b test(sdk): Update to use EventCacheStoreLockState. 2025-11-25 15:41:27 +01:00
Ivan Enderlin 9e6a6c0e71 fix(base): Use the EventCacheStoreLockState. 2025-11-25 15:41:27 +01:00
Ivan Enderlin d1633f2a78 feat(base): EventCacheStoreLockGuard can be cloned.
This patch implements `Clone` for `EventCacheStoreLockGuard`.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 4dbee471ac feat(common): CrossProcessLockGuard can be cloned.
This patch implements `Clone` for `CrossProcessLockGuard`.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 997f992d15 refactor(base): EventCacheStoreLockState owns a clone of the inner store.
This patch changes `EventCacheStoreLockState` to own a clone of
the inner store. It helps to remove the `'a` lifetime, and so it
“disconnects” from the lifetime of the store.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 3d5b32494e feat(base): Add EventCacheStoreLockGuard::clear_dirty. 2025-11-25 15:41:27 +01:00
Ivan Enderlin c5893f882c feat(common): Add CrossProcessLockGuard::is_dirty and ::clear_dirty.
This patch replicates the `is_dirty` and `clear_dirty` methods from
`CrossProcessLock` to `CrossProcessLockGuard`. It allows to get an
access to this API from a guard when one doesn't have the cross-process
lock at hand.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 68e8866bcf chore(sdk): Clean up imports. 2025-11-25 15:41:27 +01:00
Ivan Enderlin c98d9db185 feat(base) Create the EventCacheStoreLockState type.
This patch updates `EventCacheStoreLock::lock()` to return an
`EventCacheStoreLockState` instead of an `EventCacheStoreLockGuard`, so
that the caller has to handle dirty locks.
2025-11-25 15:41:27 +01:00
Ivan Enderlin cee2b1bebf feat(common): Add CrossProcessLockState::map.
This patch adds the `CrossProcessLockState::map` method along with its
companion `MappedCrossProcessLockState` type. The idea is to facilitate
the creation of custom `CrossProcessLockState`-like type in various
usage of the cross-process lock.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 19a96b41df feat(common) Add #[must_use] on CrossProcessLockGuard and *State.
This patch adds a `#[must_use]` attribute on `CrossProcessLockGuard` and
`CrossProcessLockState` to avoid a misuse.
2025-11-25 15:41:27 +01:00
Ivan Enderlin 80decaebf4 chore(common) Rename CrossProcessLockKind to CrossProcessLockState.
This patch renames the `CrossProcessLockKind` type to
`CrossProcessLockState`.
2025-11-25 15:41:27 +01:00
Jorge Martín 20ee85bd0f fix: Remove unnecessary options from sentry::ClientOptions 2025-11-25 14:19:49 +01:00
Jorge Martín 813c5fc9f9 misc: Bump Sentry SDK to v0.46.0 2025-11-25 14:19:49 +01:00
Jorge Martín a349b8e753 misc: Add support for bridge spans
These will use `bridge_trace_id` to map an exising client transaction/span to this one so they'll be displayed as a single one in Sentry.

This is done through the `sentry.trace` field, which will be used by `sentry-tracing` to differentiate these kinds of special spans.

The special fields need to be added on the Span creation, that's why we do it in the constructor instead of just using `span.record(...)` later.
2025-11-25 14:19:49 +01:00
Damir Jelić ec44c74d53 ci: Generate and upload junit files for the integration tests 2025-11-25 11:22:55 +01:00
dependabot[bot] 7475f03b13 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 10:48:12 +01:00
Ivan Enderlin f13dc4b070 doc: Add AI policy.
This patch adds AI policy. For the moment, it's a copy-paste (modulo
emphasises) from Forgejo's.
2025-11-25 10:34:11 +01:00
Ivan Enderlin 9757ff54ba doc: Format and fix the CONTRIBUTING.md document.
This patch formats the `CONTRIBUTING.md` file, plus it fixes some links,
lists, block of code etc.
2025-11-25 10:34:11 +01:00
Doug aa79e34794 chore: Add forgotten tests for removing space child.
Make sure to also check the Option inside the Result when looking for the event.
2025-11-24 17:37:33 +02:00
Doug d5f09dffaa spaces: Fix an incorrect early return introduced at the last minute. 2025-11-24 17:37:33 +02:00
Johannes Marbach ae9070815c fix(oauth): use ruma::time::instant for wasm compatibility
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-24 14:11:57 +01:00
Doug f49c588ade spaces: Add a method to get the joined parents of a given child. 2025-11-24 14:57:21 +02:00
Doug 8e0dba641d spaces: Add methods to add/remove space children. 2025-11-24 14:57:21 +02:00
Damir Jelić 2f7d2b3b9b chore: Bump our sentry-tracing deps 2025-11-21 17:00:44 +01:00
Ivan Enderlin d228bde8ef doc(ui): Merge a duplicated Refactor Section. 2025-11-21 16:17:09 +01:00
Ivan Enderlin 83a7d591bd doc(ui,ffi): Update CHANGELOG.md. 2025-11-21 16:17:09 +01:00
Ivan Enderlin 247bb4960e feat(ui,ffi): Add LatestEventValue::Local { sender, .. }.
This patch adds a `sender: OwnedUserId` field to
`LatestEventValue::Local` in `matrix_sdk_ui::timeline` (and the
corresponding `matrix_sdk_ffi` type).
2025-11-21 16:17:09 +01:00
Ivan Enderlin 83f9d74626 feat(ui,ffi): Add LatestEventValue::Local { profile, .. }.
This patch adds a `profile: TimelineDetails<Profile>` field to
`LatestEventValue::Local` in `matrix_sdk_ui::timeline` (and the
corresponding `matrix_sdk_ffi` type).
2025-11-21 16:17:09 +01:00
Damir Jelić eed5f11f26 Merge pull request #5881 from matrix-org/poljar/event-cache/fix-race-condition
Fix a race condition in the redecryptor leading to missed decryption attempts
2025-11-21 15:22:00 +01:00
Damir Jelić 75a977cc47 ci: Free up disk space for the benchmark jobs as well 2025-11-21 14:18:22 +01:00
Damir Jelić 5b396d0b0d chore: Add a link to the github issue for why async-stream isn't bumped 2025-11-21 14:18:22 +01:00
Damir Jelić 7de210a88f chore: Update the deny.toml file
The adler crate is no longer in our tree, it has been replaced by the
adler2 crate.
2025-11-21 14:18:22 +01:00
Damir Jelić 127154fcfa chore: Bump our deps and update the Cargo.lock file 2025-11-21 14:18:22 +01:00
Kévin Commaille 0e46732ede Add changelog
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-11-20 16:44:54 +00:00
Kévin Commaille 1352bd74d6 Upgrade Ruma
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-11-20 16:44:54 +00:00
Ivan Enderlin 762135ba22 doc(ui): Improve documentation of RoomListLoadingState.
This patch adds intra-links and clarifies a sentence about the room
type.
2025-11-20 14:40:48 +01:00
Damir Jelić 7a1fadddc3 doc(redecryptor): Document that we're listening to the event cache as well 2025-11-20 13:31:58 +01:00
Damir Jelić f343c98b63 fix(redecryptor): Fix race a condition where events might not be redecrypted
This patch fixes a race condition where events won't get decrypted
because a room key arrives after the initial decryption attempt but
before the UTD has been persisted in the event cache.

The fix is relatively straightforward, we'd need a synchronization point
for the two different tasks, the event cache which adds events and the
redecryptor which listens to room keys to decrypt events.

A lock could have been used, so the storing and redecrypting of events
becomes synchronized via the storage layer. This approach could have
degraded performance since the event cache needs to handle a lot of
events.

The approach that was chosen here is to let the redecryptor listen to
updates coming from the event cache itself. If the event cache tells us
that it persisted a UTD, we will attempt to decrypt. Upon a successful
decryption we will replace the event in the cache as well.
2025-11-20 13:27:58 +01:00
Damir Jelić fd4821c3ec refactor(redecryptor): Make the filter closure a function 2025-11-20 13:27:13 +01:00
Damir Jelić 5dea64b0ef feat(linked-chunk): Add method to get the items of an Update
This patch adds a convenience function for the Update enum. If one only
cares about the items contained in the Update, then they can chose to
use this method to extract them out of the enum.
2025-11-20 13:26:23 +01:00
Damir Jelić 2388acaf33 test(redecryptor): Add a test confirming a race condition in the redecryptor
This patch adds a test confirming that the redecryptor has a race
condition.

Namely, events and room keys are received over two different sync
streams from the homeserver. When events are received over the sync, we
first try to decrypt them, this might fail because the room key hasn't
yet arrived over the other sync stream. The event cache will then
persist the event as a UTD.

At the same time, the redecryptor will listen to room keys that arrive
on the other sync stream. Once the redecryptor gets notified about a
room key, it will attempt to fetch the event from the event cache to
decrypt the event and replace it.

Crucially if the key arrives before the event gets persisted but after
the initial decryption attempt we might never attempt to redecrypt such
an event.
2025-11-20 13:20:12 +01:00
Damir Jelić 91bc1ef28f test(redecryptor): Factor out common code in the redecryptor tests 2025-11-20 13:19:49 +01:00
Richard van der Hoff b1eaa5edca sdk: improve logging for received history bundles
We had an instance where a user joined a room on Element X but did not download
the key bundle, so let's add some logging to help figure out what was going on.
2025-11-19 11:38:58 +01:00
Doug 0b5e1fb9c5 xtask: Add support for building watchOS targets. 2025-11-19 11:36:53 +01:00
Ivan Enderlin 2eb4323fe1 test(ui): Test long-polling in RoomListService.
This patch tests whether long-polling is used for Sliding Sync requests
made by `RoomListService`.
2025-11-19 10:39:11 +01:00
Ivan Enderlin db806f6b8d test(ui): Simplify macro usage.
This patch declares the type of the expected value for `assert pos`.
2025-11-19 10:39:11 +01:00
Ivan Enderlin 64a51af18d feat(ui): Manually define when to do long-polling in the RoomListService.
This patch uses the newly introduced
`SlidingSyncListBuilder::requires_timeout` to define when the
`RoomListService` must apply a long-polling depending on its state
machine.
2025-11-19 10:39:11 +01:00
Ivan Enderlin da52532b60 feat(sdk): Add SlidingSyncListBuilder::requires_timeout.
This patch adds a new `SlidingSyncListBuilder::requires_timeout` method
that takes a function deciding whether the list requires a timeout, i.e.
if the list should trigger a `http::Request::timeout`, i.e. if it
deserves a long-polling or not.

The default behaviour is kept for compatibility purposes.
2025-11-19 10:39:11 +01:00
Ivan Enderlin f846eea7a3 doc(sdk): Update outdated documentation of SlidingSyncList::set_sync_mode.
This patch updates the documentation of `SlidingSyncList::set_sync_mode`
to remove an outdated reference to a `reset` method that no longer
exists.
2025-11-19 10:39:11 +01:00
Ivan Enderlin 475db3e640 refactor(sdk) Change RwLock<Observable> to SharedObservable.
This patch updates `SlidingSyncListInner::state` from a
`RwLock<Observable>` to a `SharedObservable`. It is semantically and
programmatically identical, but the API is simpler.
2025-11-19 10:39:11 +01:00
Damir Jelić efe511e5e8 Merge pull request #5869 from matrix-org/poljar/event-cache/remove-timeline-redecrypion-logic 2025-11-19 10:31:32 +01:00
Damir Jelić 4ae82dd634 feat(bindings): Allow user identities to only be fetched from storage 2025-11-19 09:42:26 +01:00
Jorge Martin Espinosa d860749f95 Revert "doc: Add warnings about overriding the server URL"
This reverts commit 95d8ba94e1.
2025-11-18 15:58:19 +01:00
Jorge Martin Espinosa 012a9825a4 Revert "refactor(ffi): Remove unused Session::homeserver_url value"
This reverts commit 4eb3cc9812.
2025-11-18 15:58:19 +01:00
Jorge Martín 1c22d0b25b doc: add changelogs 2025-11-18 12:26:30 +01:00
Jorge Martín be86fe4aa9 doc: Improve doc comments
Also move `EventMeta::thread_root_id` next to `event_id`
2025-11-18 12:26:30 +01:00
Jorge Martín 385f7aa86d doc: Fix docs for ffi::Timeline::latest_event_id 2025-11-18 12:26:30 +01:00
Jorge Martín 5f996f77c6 reafactor(ffi): Have ffi::Timeline::latest_event_id use ui::Timeline::latest_event_id, instead of ui::Timeline::latest_event
This is important because `latest_event` would also return local events, which won't have an event id.
2025-11-18 12:26:30 +01:00
Jorge Martín 02491fc6ec test: Add test for TimelineController::latest_event_id 2025-11-18 12:26:30 +01:00
Jorge Martín 0f62ff991d fix clippy 2025-11-18 12:26:30 +01:00
Jorge Martín 6b245264e1 fix(test): Fix broken test locally: it was using a previous cached value before 2025-11-18 12:26:30 +01:00
Jorge Martín f7b92c84e7 fix(ui): Sending read receipt in live timeline when latest event is in a thread
Previously, this used the latest event in the thread as the event to mark as read, while this is not right if we're in a context that hides thread events
2025-11-18 12:26:30 +01:00
Jorge Martín 4eb3cc9812 refactor(ffi): Remove unused Session::homeserver_url value 2025-11-18 12:16:28 +01:00
Jorge Martín 95d8ba94e1 doc: Add warnings about overriding the server URL
This may be dangerous when done while restoring an existing session.
2025-11-18 12:16:28 +01:00
Andy Balaam ca436016b4 base: Bump ruma to 91424b1fc
And update to reflect the new feature name unstable-msc4362, which
provides the new unstable prefix io.element.msc4362.encrypt_state_events
2025-11-18 11:10:55 +00:00
Andy Balaam 5b82550199 crypto: Wait for a stream in state encryption test
This was sometimes failing for me locally, so use a macro that expects a
value from a stream soon, rather than immediately.
2025-11-18 11:10:55 +00:00
Andy Balaam 5d396e4795 crypto: Refer to MSC4362 when we are talking about encrypted state 2025-11-17 09:40:47 +02:00
Damir Jelić e9c8f101d6 chore: Remove the various redecrytion tasks 2025-11-14 12:54:00 +01:00
Damir Jelić 6e97607c2d refactor(timeline): Replace the various decryption tasks with one R2D2 task 2025-11-14 12:54:00 +01:00
Damir Jelić 4e71b7c351 feat(ui): Create a task to listen to redecryptor reports in the timeline
This task is still necessary because the redecryptor in the event cache
might miss some room keys.

In this case the timeline can tell the redecryptor which events it
should retry to decrypt.

We're collecting all the UTDs in the timeline and telling the
redecryptor to do its best.
2025-11-14 12:52:44 +01:00
Richard van der Hoff 9ab886fa2b crypto: Merge inbound Megolm sessions [#5865]
When we receive two copies of the same inbound Megolm session from two sources, merge them together intelligently.

Fixes: #5108, #4698
2025-11-13 19:06:44 +00:00
Richard van der Hoff 60072b3456 Integ test for merging megolm sessions with history sharing
Add an integration test that checks that, when we receive a copy of a megolm
session directly after having previously received it via history sharing, we
get the best bits of both.
2025-11-13 18:37:18 +00:00
Richard van der Hoff 822b1c9787 crypto: replace uses of compare_group_session
... with `merge_received_group_session`.

`merge_received_group_session` expands the logic of `compare_group_session` to
handle the fact that there is more than one axis of "better" or "worse" and we
may need to take the best bits of two copies of the session.
2025-11-13 18:37:18 +00:00
Richard van der Hoff 52344fad77 crypto: Add new method Store::merge_received_group_session
Add a method which can be used to merge a received `InboundGroupSession` into
whatever we find in the store.
2025-11-13 18:37:18 +00:00
Damir Jelić e0427767aa refactor(timeline): Use the event cache to request redecryption 2025-11-13 16:57:36 +01:00
Damir Jelić 927c82f97a refactor(timeilne): Add a method to compute redecryption candidates 2025-11-13 16:56:36 +01:00
Richard van der Hoff 97ba0b1bbb crypto: factor out InboundGroupSession.compare_ratchet
In order to correctly merge sessions, we need more granular comparisons between
two sessions than just "Better" or "Worse", so factor out a method that *just*
looks at the ratchet states.
2025-11-13 13:51:25 +00:00
JoFrost 17df3f84d0 feat(ffi): expose join_rules in OtherState::RoomJoinRules (#5863)
Expose the room join rules in the `OtherState::RoomJoinRules` event 
for the FFI timeline.

It reuses the existing `JoinRules` type from the client module and
converts the event content accordingly. This allows clients to inspect
the room’s current join rule directly from the event. Like `m.federate`,
this field was previously unavailable in the FFI variant of the SDK.

---------

Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2025-11-13 15:20:17 +02:00
Damir Jelić 4fbc83af44 Merge pull request #5746 from matrix-org/poljar/event-cache/redecryptor 2025-11-13 12:19:51 +01:00
Damir Jelić 9508675aca fix(redecryptor): Early return if we don't have any events to process 2025-11-13 11:59:21 +01:00
Damir Jelić 0d08ed0758 refactor(redecryptor): Add some type aliases for the event ID/event tuples 2025-11-13 11:59:21 +01:00
Damir Jelić 913ebe9fa9 docs(redecryptor): Clarify that we're talking about the UI timeline in the r2d2 docs 2025-11-13 11:59:05 +01:00
Jorge Martin Espinosa f702364fe9 feat(sdk): Add a power level value field for StateEventType::SpaceChild (#5857)
Closes https://github.com/matrix-org/matrix-rust-sdk/issues/5839

Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
2025-11-13 07:17:57 +00:00
Jonas Platte 1db4a4cb9a Use MSRV-aware resolver 2025-11-12 14:58:30 +01:00
Damir Jelić 2e9e9aedd7 chore(redecryptor): Ensure the upgrade_event_cache method is inlined 2025-11-12 13:31:37 +01:00
Damir Jelić 38df621b8a chore(event-cache): Limit the visibility of post_process_new_events 2025-11-12 13:31:37 +01:00
Damir Jelić f9c23b3612 refactor(redecryptor): Use an abort handle to manage the redecryption task 2025-11-12 13:31:37 +01:00
Damir Jelić 952c5af07c chore(redecryptor): Time how long it takes to replace UTDs 2025-11-12 12:56:21 +01:00
Damir Jelić 717f016f21 docs(redecryptor): Add some docs to the Redecryptor struct itself 2025-11-12 12:55:58 +01:00
Damir Jelić 3ad70623bb chore(redecryptor): Use relative imports more often 2025-11-12 12:55:19 +01:00
Damir Jelić 84a21a42d0 fix(event-cache): Don't hold on to the event cache locks as long when fetching events 2025-11-12 12:54:32 +01:00
Damir Jelić d2eab603c1 fix(event-cache): Limit the visibility of room_linked_chunk_mut a bit better 2025-11-12 12:51:35 +01:00
Andy Balaam 8883b9db5a Improve the wording of error messages when redecryption fails
The previous message implied that we had received a session for this
message, but that is only one of the several reasons we might encounter
this situation. If redecryption failed, it is more likely we got here
because we'd been asked to attempt redecryption for all UTDs e.g. when
we build a new timeline.

Additionally, having similar wording for the error case and the unable
to decrypt case could also cause confusion, so I adjusted the wording to
make clear which situation is happening.
2025-11-12 11:47:55 +00:00
Ivan Enderlin a3424a7c4a feat(base): Explicitly handle the CrossProcessLockKind::Dirty case in MediaStore.
This patch replaces the `into_guard()` call by a `match` over
`CrossProcessLockKind` so that the `Dirty` case is explicitly handled.

The mid-term idea is to remove the `into_guard()` method because it
is “dangerous” as it hides the `Dirty` case.
2025-11-11 15:12:27 +01:00
Ivan Enderlin fa3ca980e9 doc(common): Explain how to clear a dirty cross-process lock. 2025-11-11 15:12:27 +01:00
Jorge Martín cbd4722dcb doc: Add changelog entry 2025-11-11 14:50:00 +01:00
Jorge Martín a22caa32c0 misc: Add better default target-feature values for Android in ARM64 devices 2025-11-11 14:50:00 +01:00
Damir Jelić f61ba4f47c fix(ui): Don't do a authenticated /versions call when building the roomlist service 2025-11-11 14:37:10 +01:00
Damir Jelić 9a3857d3a7 feat(client): Add a method to only get the server versions from the cache 2025-11-11 14:37:10 +01:00
Ivan Enderlin e79f832160 fix(ui): Undo an optimisation to start at SettingUp.
This patch undo an optimisation that was initialising the
`RoomListService` at the `SettingUp` state if a `pos` value was
recovered successfully (see bbf9bf2c0b).
The problem is that it starts with a range of 0..99 instead of 0..19,
which can slow things done in particular cases. Whilst a good idea on
paper, it's not in practise. So let's continue to recover the `pos`, but
let's keep starting at the `Init` state.
2025-11-11 13:12:02 +01:00
Ivan Enderlin 46d05d877b fix(base): Remove a panic in a log.
We must not panic if the event has no event ID.
2025-11-11 11:15:59 +01:00
Ivan Enderlin 610f82aeb2 chore(sqlite): Remove connection::Config.
This patch removes the `connection::Config` type. It was “inspired”
from `deadpool_sqlite`, but we can clearly remove it by using our own
`SqliteStoreConfig` type. It simplifies the way we open a database.
2025-11-11 11:09:18 +01:00
Ivan Enderlin 60490f4eff doc(sqlite): Add documentation to connection.
This patch explains why we create our own implementation of `deadpool`
for `rusqlite`.
2025-11-11 11:09:18 +01:00
Ivan Enderlin 6a828e31dd feat(sqlite): Replace deadpool-sqlite by our own implementation.
This patch replaces `deadpool-sqlite` by our own implementation in
`crate::connection`. It still uses `deadpool` but the object manager has
a different implementation.
2025-11-11 11:09:18 +01:00
dependabot[bot] fff270d997 chore(deps): bump CodSpeedHQ/action from 4.3.1 to 4.3.3
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.3.1 to 4.3.3.
- [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/4348f634fa7309fe23aac9502e88b999ec90a164...bb005fe1c1eea036d3894f02c049cb6b154a1c27)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-11 09:19:04 +01:00
Johannes Marbach a50ecb5b18 refactor(oauth): remove superfluous join in QR login tests
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-11 09:18:16 +01:00
Ivan Enderlin 18654444b6 doc(sdk): Remove a dead reference in the doc.
This patch removes the reference to `Update`, that is no longer required.
2025-11-11 09:17:34 +01:00
dependabot[bot] 10ff5d0cc6 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from e7d460faa33cbba452e69e8b1700e6a75e8a72b8 to 04b9adbd8c1c00963289b5628510dd907b27dc60.
- [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/e7d460faa33cbba452e69e8b1700e6a75e8a72b8...04b9adbd8c1c00963289b5628510dd907b27dc60)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 18:26:12 +01:00
Johannes Marbach f9584f5b2a feat(ffi): add sender and room information to sync notifications
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-08 09:30:11 +01:00
Damir Jelić 66619e9d1d test(oauth): Pass the rendezvous server to the bob task as well
This avoids the scenario where the mock server gets deallocated before
the rendezvous server and thus the rendezvous specific mock guards.

Dropping those in the wrong order will result in a panic.
2025-11-08 09:29:30 +01:00
Damir Jelić 2ea1c42a1a test(oauth): No need to use join in the qrcode login granting tests 2025-11-08 09:29:30 +01:00
Damir Jelić f6ef5fbfd1 chore: Remove a stale TODO item 2025-11-08 09:29:30 +01:00
JoFrost a6062a6cfd feat(ffi): expose m.federate and history visibility in their events (#5830)
Hello, I'm writing on behalf of the Citadel product developed by ERCOM.
This PR expose `m.federate` and `history_visibility` in timeline diffs.
These fields are available in the Matrix SDK but were previously omitted
from the FFI variant.

Signed-off-by: JoFrost <20685007+JoFrost@users.noreply.github.com>
2025-11-07 16:16:17 +01:00
Ivan Enderlin 3f3f6c2fc6 refactor(common): Revisit CrossProcessLock::try_lock_once and spin_lock's outputs.
This patch changes the signature of `CrossProcessLock::try_lock_once`.
It was returning a:

```rust
Result<CrossProcessLockResult, CrossProcessLockError>
```

Now it returns a:

```rust
Result<Result<CrossProcessLockKind, CrossProcessLockUnobtained>, L::LockError>
```

We will explain these new types in a moment.

This patch also changes the signature of `CrossProcessLock::spin_lock`.
It was returning a:

```rust
Result<CrossProcessLockGuard, CrossProcessLockError>
```

Now it returns a:

```rust
Result<Result<CrossProcessLockKind, CrossProcessLockUnobtained>, L::LockError>
```

First off, we notice that the returned types are now unified. The
`CrossProcessLockResult` type has been renamed `CrossProcessLockKind`
and lives in a `Result::Ok`. The `CrossProcessLockResult::Unobtained`
variant has been removed, but `CrossProcessLockUnobtainedReason`
has been renamed to `CrossProcessLockUnobtained` and lives in a
`Result::Err`.

Second, the `CrossProcessLockError` now is a union type between
`CrossProcessLockUnobtained` and `TryLock::LockError`. It's not used
by `try_lock_once` or `spin_lock`, but only by the code using the
cross-process lock to provide a unified error type.

The ideas behind these changes are:

- it's easy to forward an error from the `TryLock`,
- it's difficult to ignore the `Clean` vs. `Dirty` state of the lock
  guard,
- unified API with clearly separated responsibility (the first `Result`
  vs. the second `Result`).

Note: the `CrossProcessLockKind::into_guard` method aims at being
removed. It's useful now to maintain compatibility but it's “dangerous”
as it makes trivial to skip `Clean` vs. `Dirty` states. We ultimately
don't want that.
2025-11-07 16:13:03 +01:00
Damir Jelić d7d4730b21 docs(redecryptor): Document the redecryptor a bit more 2025-11-07 15:38:35 +01:00
Damir Jelić 4c4cd41457 test(timeline): Workarounds to get the timeline tests passing
This is necessary because both the timeline and the event cache attempt
to redecrypt events currently.

This will change once only the event cache handles this task.
2025-11-07 15:38:35 +01:00
Damir Jelić 4a519bd547 test(redecryptor): More tests for the redecryptor 2025-11-07 15:38:35 +01:00
Damir Jelić 4109fddc97 feat(redecryptor): Post-process the events once they are replaced 2025-11-07 15:38:35 +01:00
Damir Jelić 7e98858815 feat(redecryptor): More precise logs for the redecryption attempts 2025-11-07 15:38:35 +01:00
Damir Jelić 3a0a5b9888 feat(redecryptor): Use the room to redecrypt events
This allows us to properly calculate the push actions.
2025-11-07 15:38:35 +01:00
Damir Jelić 621d936b4c feat(redecryptor): Let the redecryptor listen to room key withheld updates 2025-11-07 15:38:35 +01:00
Damir Jelić a2f89e85b9 feat: Redecryptor start to send out redecryptor reports 2025-11-07 15:38:35 +01:00
Damir Jelić 4ed239351a feat(event cache): Enable the redecryptor in the event cache 2025-11-07 15:38:35 +01:00
Damir Jelić 5c3bca86a4 doc(event cache): Document the redecryptor 2025-11-07 15:38:35 +01:00
Damir Jelić e934235045 feat(redecryptor): Rejigger things so we can relisten to the room key stream 2025-11-07 15:38:35 +01:00
Damir Jelić f2cc6c650a test(redecryptor): Add a test to show that the redecryptor works 2025-11-07 15:38:35 +01:00
Damir Jelić 8103b9cc23 feat(event cache): Create the redecryptor 2025-11-07 15:29:07 +01:00
Damir Jelić d3c839a2d0 feat(event cache): Add a method to access the linked chunk mutably 2025-11-07 15:14:42 +01:00
Ivan Enderlin 3b1418463b doc(common): Fix a link in the CHANGELOG.md. 2025-11-07 11:26:09 +01:00
Ivan Enderlin 9f248affa9 doc(common): Update CHANGELOG.md. 2025-11-07 11:26:09 +01:00
Ivan Enderlin edf7604d30 feat(common): Detect when the cross-process lock has been dirtied.
This patch detects when the cross-process lock has been dirtied.

A new `CrossProcessLockResult` enum is introduced to simplify the
returned value of `try_lock_once` and `spin_lock`. It flattens the
previous `Result<Option<_>>` by providing 3 variants: `Clean`, `Dirty`
and `Unobtained`.
2025-11-07 11:26:09 +01:00
Ivan Enderlin f7a767ce97 feat(indexeddb): Add Lease::generation in crypto, media, and event cache stores.
This patch adds `Lease::generation` support in the crypto, media and
event cache stores.

For the crypto store, we add the new `lease_locks` object store/table.
Previously, `Lease` was stored in `core`, but without any prefix, it's
easy to overwrite another records, it's dangerous. The sad thing is
that it's hard to delete the existing leases in `core` because the keys
aren't known. See the comment in the code explaining the tradeoff.

For media and event cache stores, the already existing `leases` object
store/table is cleared so that we can change the format of `Lease`
easily.
2025-11-07 11:26:09 +01:00
Ivan Enderlin 6c922e69d0 feat(common): Add a cross-process lock generation.
This patch adds `CrossProcessLockGeneration`. A lock generation is an
integer incremented each time the lock is taken by another holder. If
the generation changes, it means the lock is _dirtied_. This _dirtying_
aspect is going to be expanded in the next patches. This patch focuses
on the introduction of this _generation_.

The `CrossProcessLock::try_lock_once` method, and
the `TryLock::try_lock` method, both returns a
`Option<CrossProcessLockGeneration>` instead of a `bool`: `true` is
replaced by `Some(_)`, `false` by `None`.
2025-11-07 11:26:09 +01:00
Ivan Enderlin 01d75e939c chore(indexeddb): Run rustfmt. 2025-11-07 11:26:09 +01:00
Jorge Martín 8b805b1ea5 refactor: Try to avoid filtering all event items before finding one with the wanted id 2025-11-07 10:42:53 +01:00
Jorge Martín a1768ea518 refactor: Add profile cache for handle_remote_event 2025-11-07 10:42:53 +01:00
Johannes Marbach 0b66019632 feat(ffi): add bindings for granting login with a QR code
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-06 15:15:59 +01:00
Richard van der Hoff c064ca8b18 Merge pull request #5834 from matrix-org/rav/history_sharing/fix_withheld_utd_cause
crypto: correct UtdCause for unshared historical messages
2025-11-06 11:45:06 +00:00
Jorge Martín fa6d18b55f refactor(sdk): Make the deserialization of the ignored users happen in parallel too 2025-11-06 11:13:23 +01:00
Jorge Martín 17de97e98e refactor(sdk): Fetch member data concurrently
Creating a `RoomMember` takes a lot of store queries, and previously all of them were done sequentially. I've tried to make this process run as much in parallel as I can.
2025-11-06 11:13:23 +01:00
Richard van der Hoff c60f92a917 crypto: correct UtdCause for unshared historical messages
Per https://github.com/element-hq/element-meta/issues/2876, we want messages
where the history was not shared to appear the same as regular "historical"
messages.
2025-11-05 15:08:10 +00:00
Richard van der Hoff 0865e96f08 refactor(crypto): simplify UtdCause logic
I find a single match statement easier to reason about than one nested in another.

Also, import `UnableToDecryptReason::*`, to shorten the match lines.
2025-11-05 15:08:10 +00:00
Richard van der Hoff 8f726e4fb9 test: use a Timeline for shared_history integ tests
I want to be able to test that the correct `UtdCause` is presented for withheld
historical messages. That means we need to use `/sync` rather than `/event` to
obtain the message (since the MSC4115 `membership` field is missing on `/event`
(https://github.com/element-hq/synapse/issues/17486)). So then the most
realistic way to get hold of the actual UtdCause is to use a Timeline.

Of course, the thing I actually want to test doesn't actually work correctly,
so it's left as a FIXME in this commit.
2025-11-05 15:08:10 +00:00
Johannes Marbach b4ebc8bc25 feat(oauth): add flow for granting login by scanning a QR code
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 13:27:28 +01:00
Johannes Marbach da1369b9c2 refactor(oauth): rename request_login to request_login_with_scanned_qr_code to avoid future name clashes for the opposite flow
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 13:27:28 +01:00
Johannes Marbach d122f10147 fix(oauth): fix doc comment for GrantLoginWithGeneratedQrCode::subscribe_to_progress
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 13:27:28 +01:00
Johannes Marbach bcf81c89e9 refactor(oauth): make device creation timeout configurable and use a lower value for tests to speed them up
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 13:27:28 +01:00
Johannes Marbach d3dd9d28c8 refactor(oauth): extend doc comment of GrantLoginWithQrCodeBuilder::generate for better usability and to match the login flow
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 13:27:28 +01:00
Johannes Marbach dcd08e8d3b refactor(oauth): move QrProgress to module file for later reuse
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 13:27:28 +01:00
Johannes Marbach 82c583b5bc feat(ffi): expose Client::register_notification_handler
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-11-05 10:09:31 +01:00
Ivan Enderlin 81ff96d569 fix(sqlite): Fix the database version.
The database has been updated but the version hasn't been bumped.
2025-11-04 14:59:15 +01:00
Damir Jelić 49db60d951 feat: Allow events to be fetched by event type 2025-11-04 13:58:49 +01:00
Damir Jelić 8f4267332a test: Allow to create encrypted events in the event factory 2025-11-04 13:58:49 +01:00
Damir Jelić 950c42742d refactor(sqlite): Save the event type of an event in the SQLite event cache 2025-11-04 13:58:49 +01:00
Damir Jelić f91ffb4c31 feat: Add a method to get the event type of a TimelineEventKind 2025-11-04 13:58:49 +01:00
Richard van der Hoff 301ca5e2b8 Fix up changelogs incorrectly updated since 0.14.0 (#5828)
All of these entries have been incorrectly added to the changelogs
*since* 0.14.0 was released :(
2025-11-04 12:50:49 +00:00
Doug 1a384f0049 xtask: Workaround UniFFI's noHandle generation for Swift.
https://github.com/mozilla/uniffi-rs/issues/2717
2025-11-04 14:11:14 +02:00
Damir Jelić 781df5526d Revert "fix: Allow /versions requests to refresh the token"
This reverts commit 05b40af2c1.
2025-11-04 09:50:37 +01:00
dependabot[bot] ea07d0199a chore(deps): bump crate-ci/typos from 1.38.1 to 1.39.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.38.1 to 1.39.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.38.1...v1.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 15:46:41 +01:00
dependabot[bot] ddfd2fb570 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 53dce01c203a6a857c9544ebec630a370d596d65 to e7d460faa33cbba452e69e8b1700e6a75e8a72b8.
- [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/53dce01c203a6a857c9544ebec630a370d596d65...e7d460faa33cbba452e69e8b1700e6a75e8a72b8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-03 15:41:54 +01:00
Richard van der Hoff 3b5f1eee27 Merge pull request #5820 from matrix-org/rav/history_sharing/transitive_withheld_code
crypto: pass on "history_not_shared" withheld notifications
2025-11-03 12:58:04 +00:00
Richard van der Hoff 99ae08ebfe Merge remote-tracking branch 'origin/main' into rav/history_sharing/transitive_withheld_code 2025-11-03 12:15:52 +00:00
Richard van der Hoff 9efb0de4d7 Merge pull request #5819 from matrix-org/rav/cryptostore_withheld_sessions_by_room_id
crypto: add new `CryptoStore` method `get_withheld_sessions_by_room_id`
2025-11-03 12:13:48 +00:00
Damir Jelić 05b40af2c1 fix: Allow /versions requests to refresh the token 2025-10-31 15:58:44 +01:00
Damir Jelić 09ee1375cd fix: Skip authorization headers when doing a /versions while doing a token refresh 2025-10-31 15:58:44 +01:00
Damir Jelić a96485c07a test: Test that we don't end up in a deadlock when fetching the version 2025-10-31 15:58:44 +01:00
Damir Jelić 9680fc3a0f test: Test that the skip_auth option works correctly 2025-10-31 15:58:44 +01:00
Damir Jelić 422f925033 feat: Allow authorization headers to be skipped with the RequestConfig 2025-10-31 15:58:44 +01:00
Richard van der Hoff 3695d76dec crypto: pass on "history_not_shared" withheld notifications
When constructing a key bundle, if we had received a key bundle ourselves, in
which one or more sessions was marked as "history not shared", pass that on to
the new user.
2025-10-31 12:00:06 +00:00
Richard van der Hoff 0faf3eecea update changelogs 2025-10-31 12:00:06 +00:00
Richard van der Hoff 13a30f7b7a crypto: test for CryptoStore::get_withheld_sessions_by_room_id
integration test for the new method
2025-10-31 12:00:06 +00:00
Richard van der Hoff 444fcfa098 stores: new method CryptoStore::get_withheld_sessions_by_room_id
Implement this across all the store implementations
2025-10-30 23:10:05 +00:00
Richard van der Hoff cadbd33957 sqlite: add room_id index on direct_withheld_info table 2025-10-30 18:50:24 +00:00
Richard van der Hoff 8189010d58 indexeddb: invert key order for withheld sessions
... in preparation for extracting all withheld sessions for a given room.
2025-10-30 18:50:24 +00:00
Richard van der Hoff ee828614fb Merge pull request #5807 from matrix-org/rav/history_sharing/not_shared_code
crypto: use a new withheld code when history is marked as "not shareable"
2025-10-30 15:12:57 +01:00
Richard van der Hoff ef3c6719cf test: integ test for withhelds in history sharing
Add an integration test that ensures that the correct withheld code is sent
when history is marked as "not shareable"
2025-10-30 13:58:38 +00:00
Richard van der Hoff 55ef066eb4 crypto: use new withheldcode when we encounter unshareable sessions 2025-10-30 13:58:38 +00:00
Richard van der Hoff e3105bfca8 crypto: define new WithheldCode for MSC4268 2025-10-30 13:58:38 +00:00
Johannes Marbach 9fff07dfbb feat(oauth): add flow for reciprocating a login using a QR code generated on the existing device
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-29 20:37:22 +01:00
Johannes Marbach ce7f2fb24f refactor(secure_channel): rename SecureChannel::new to SecureChannel::reciprocate and make it available outside of tests
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-29 20:37:22 +01:00
Johannes Marbach b60b042cfe feat(testing): add mock for get device endpoint
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-29 20:37:22 +01:00
Damir Jelić 046d8ebdd1 test: Allow to omit the timeout for assert_recv_with_timeout (#5814)
Add some documentation to it while we're at it as well.
2025-10-29 15:19:37 +00:00
Damir Jelić 896f4114a2 chore(sqlite): Don't log the room ID twice when saving events
The room ID is already logged as part of the span due to the instrument
attribute.
2025-10-29 15:58:41 +01:00
Damir Jelić e2d42cef67 test: Add some spans to distinguish which user is mocking up the encryption 2025-10-29 15:54:47 +01:00
Ivan Enderlin 12e39f5ef1 chore(ffi): Restore ClientBuilder::session_paths as #[deprecated].
This method restores and marks `ClientBuilder::session_paths` as
deprecated.
2025-10-29 15:28:20 +01:00
Ivan Enderlin 38875b021d chore(ffi): Allow clippy::result_large_err.
These two methods are used only once, it's fine to get a large error
here.
2025-10-29 15:28:20 +01:00
Ivan Enderlin 0bbfc3ce41 doc(ffi): Update CHANGELOG.md and README.md. 2025-10-29 15:28:20 +01:00
Ivan Enderlin 7c6ff517d5 feat(ffi): Add IndexedDB and in-memory session stores support.
This patch introduces the `sqlite` and `indexeddb` feature flag,
enabling the use of SQLite or IndexedDB for the stores. This patch also
introduces the ability to use non-persistent, in-memory stores.

The new `ClientBuilder::in_memory_store`, `ClientBuilder::sqlite_store`
and `ClientBuilder::indexeddb_store` methods are introduced to
configure the stores. This patch adds new `SqliteStoreBuilder` and
`IndexedDbStoreBuilder` structure.
2025-10-29 15:28:20 +01:00
Kévin Commaille 8e25c36289 Upgrade Ruma (#5815)
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-29 15:02:27 +01:00
Damir Jelić 200fde8850 chore: Convert a Note to a NOTE
All uppercase is the correct convention and some editors even highlight
things if the correct convention is used.
2025-10-29 15:01:48 +01:00
Damir Jelić d4e0ec302a chore: Fix a comment 2025-10-29 15:00:38 +01:00
Jorge Martín c3e01a6902 doc: Add changelog 2025-10-29 10:08:03 +01:00
Jorge Martín 25b1c85998 feat(ffi): Upgrade UniFFI to v0.30.0 2025-10-29 10:08:03 +01:00
Richard van der Hoff 5a5b8afd4a crypto: add logging for withheld data in key bundles 2025-10-28 12:37:08 +00:00
Jorge Martín 9af8fad880 doc: Add changelogs 2025-10-28 10:57:31 +01:00
Jorge Martín b748148d36 fix(ui): Make Timeline::latest_event always return the latest event, not the latest item if it's an event
This matches the usages of `latest_event_id` in other parts of the SDK.
2025-10-28 10:57:31 +01:00
Jorge Martín 513a69c547 feat(ffi): Add Timeline::latest_event_id
It will allow us to fetch the latest event id coming from the SDK instead of deciding which one to use in the clients, which could be altered by filters, post-processing, etc.
2025-10-28 10:57:31 +01:00
Jorge Martín 2f58109853 feat(ffi): Add Room::mark_as_fully_read_unchecked
This method shouldn't be widely used, but it's useful when we want to mark the room as fully read when leaving it and at the same time we have to destroy the room and timeline instances immediately so their in-memory cache is cleared
2025-10-28 10:57:31 +01:00
dependabot[bot] deda2ec75a chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 026132adc2b95c4f16b8c2943d14aedb731daadc to 53dce01c203a6a857c9544ebec630a370d596d65.
- [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/026132adc2b95c4f16b8c2943d14aedb731daadc...53dce01c203a6a857c9544ebec630a370d596d65)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:02:13 +01:00
dependabot[bot] 01a0e136dc chore(deps): bump CodSpeedHQ/action from 4.2.1 to 4.3.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.2.1 to 4.3.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/c6574d0c2a990bca2842ce9af71549c5bfd7fbe0...4348f634fa7309fe23aac9502e88b999ec90a164)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:01:41 +01:00
dependabot[bot] 5ab792e68e chore(deps): bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 17:01:16 +01:00
Michael Goldenberg 89d46cd342 doc(indexeddb): add changelog entry for separating media content and metadata in IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 155a7b481b refactor(indexeddb): use UUID instead of u64 as media content id
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 0ac943b4c4 refactor(indexeddb): rename MediaContent::id -> MediaContent::content_id
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 18fe2b20e6 doc(indexeddb): fix typos in documentation
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 64a0f62631 refactor(indexeddb): remove (de)serialization functionality from top-level media type
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg c169cab3b0 refactor(indexeddb): remove media object store and associated types
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 91858e0913 refactor(indexeddb): simplify error type for media metadata impl of indexed
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg f70c036ff9 refactor(indexeddb): re-implement media-related fns in terms of media metadata and media content stores
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 70d6d557ca refactor(indexeddb): implement specialized fn for getting media metadata keys via generalized fn
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg a52df18740 refactor(indexeddb): add transaction fns for getting media metadata keys by index
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg ce6ef90f74 refactor(indexeddb): rename transaction fn for getting all media metadata keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 5f00e71f5f refactor(indexeddb): add content id to media metadata keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 539bd9c79a refactor(indexeddb): add constants for media content id bounds
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 23785a3023 refactor(indexeddb): remove unused type synonym
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 3d31b81abf refactor(indexeddb): add type synonym for content id in indexed media content key
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 3b2ef02749 refactor(indexeddb): add fn for prefixed key ranges from existing key ranges
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg c887819809 refactor(indexeddb): return indexed type from Transaction::{put_item,put_item_if} and its derivatives
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 780b782660 refactor(indexeddb): return indexed type from Transaction::add_item and its derivatives
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 4d4ae79b7a refactor(indexeddb): return indexed type and js value from indexed type serializer
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg af0a3aa91b refactor(indexeddb): add transaction fns for deleting media metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 84e5ce0a98 refactor(indexeddb): add transaction fns for add/putting media metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 574df8951e refactor(indexeddb): add transaction fns for getting media metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 6ff186b744 refactor(indexeddb): add indexed types and keys for media metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 6036f19af6 refactor(indexeddb): add migrations for media metadata store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg f78bac2fc6 refactor(indexeddb): add content id and content size to media metadata
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 1ae3b79c08 refactor(indexeddb): flatten nested media metadata into media type
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg b4d702f1ef refactor(indexeddb): add transaction fns for getting the next available media content id
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg c258368925 refactor(indexeddb): add key bounds for media content id key
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 43f19e411a refactor(indexeddb): add constant for representing safe bounds of u64
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 4e8ddde2f2 refactor(indexeddb): add transaction fn for getting max key in range
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg dfb3713f1e refactor(indexeddb): add transaction fns for basic media content operations
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 105fa53a4c refactor(indexeddb): add indexed types for media content
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 7238d3ca23 refactor(indexeddb): remove indexed media content type synonym
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 3c522f9505 refactor(indexeddb): add type for tracking media content and associated id
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Michael Goldenberg 0796b71bd3 refactor(indexeddb): add migrations for media content store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-27 16:59:00 +01:00
Kévin Commaille 547ab31b82 bonus(sdk): Add more profile tests
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-27 10:51:45 +01:00
Kévin Commaille 3f5d51a203 Add changelog for extended profile fields
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-27 10:51:45 +01:00
Kévin Commaille 4ea0b7d984 refactor(sdk): Prefer DELETE HTTP method for profile fields
When it is supported by the homeserver.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-27 10:51:45 +01:00
Kévin Commaille d2faa1be1a feat(sdk): Add support for deleting custom profile fields
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-27 10:51:45 +01:00
Kévin Commaille ca0929876f feat(sdk): Add support for setting custom profile fields
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-27 10:51:45 +01:00
Kévin Commaille c9d3088701 feat(sdk): Add support for fetching custom profile fields
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-27 10:51:45 +01:00
Johannes Marbach 68b902e4bc feat(ffi): add bindings for listening to global send queue updates
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-24 17:43:29 +02:00
Richard van der Hoff 8bb8bbae9c Merge pull request #5737 from matrix-org/kaylendog/shared-history/store
When we receive a key bundle, add any `withheld` data to the crypto store.
2025-10-24 17:31:46 +02:00
Damir Jelić 3733ee8534 chore: Remove the matrix-sdk-crypto re-export in the matrix-sdk crate 2025-10-24 16:37:15 +02:00
kaylendog b045462f76 feat: Append withheld info from room key bundle to store. 2025-10-24 14:29:35 +01:00
Richard van der Hoff 8bb5e501a4 test(crypto): use MegolmV2 in tests where experimental-algorithms are enabled 2025-10-24 13:19:37 +01:00
Richard van der Hoff 7aead98863 refactor(crypto): Split receive_room_key_bundle to helper methods
Split out session import logic to `import_room_key_bundle_sessions`.
2025-10-24 13:09:20 +01:00
kaylendog 7607c4ef82 tests: Test deserializing m.room_key.withheld to withheld entry.
Tests that a to-device `m.room_key.withheld` event can be
serialized (using JSON), then deserialized as a RoomKeyWithheldEntry.
Ensures compatibility with exisiting store data.
2025-10-24 13:09:17 +01:00
Skye Elliot 02fe0c9f53 feat: Add RoomKeyWithheldEntry to wrap to-device and bundle payloads. 2025-10-24 12:33:36 +01:00
Stefan Ceriu d117532fae feat(spaces): add support for MSC3230 and top level space order (#5799)
This is an unstable feature but as per
[MSC3230](https://github.com/matrix-org/matrix-spec-proposals/pull/3230)
each space room might have an optional
`m.space_order`/`org.matrix.msc3230.space_order` string field in its
room account data defining the lexicographical order in which the spaces
should be displayed, with spaces missing this field shown at the bottom
and ordered by their room id.
2025-10-24 12:18:29 +03:00
dependabot[bot] 34c5e24b72 chore(deps): bump actions/setup-node from 5 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 09:41:23 +02:00
dependabot[bot] 5bdb7ae732 chore(deps): bump CodSpeedHQ/action from 4.1.1 to 4.2.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.1.1 to 4.2.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/6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9...c6574d0c2a990bca2842ce9af71549c5bfd7fbe0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 09:40:54 +02:00
dependabot[bot] 5729ad4dd5 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 744a6d5e0db5d189ad36edb08c5f77107cc42310 to 026132adc2b95c4f16b8c2943d14aedb731daadc.
- [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/744a6d5e0db5d189ad36edb08c5f77107cc42310...026132adc2b95c4f16b8c2943d14aedb731daadc)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-24 09:40:31 +02:00
Stefan Ceriu d36b68b7d1 fix(spaces): have space children with an order field set come before the others in room lists 2025-10-22 13:51:48 +03:00
Johannes Marbach 34d71b0392 feat(composer): add support for attachments in drafts
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-21 11:00:24 +01:00
Damir Jelić 430304f392 chore: Rewrite timeline redecryption tests to use HTTP mocking
This is important since we want to move the redecryption logic out of
the timeline into the main crate. This in turn means that we don't have
such low level access to the redecryption logic.

Not all tests were rewritten:
    - `test_retry_edit_and_more` Is proving to be difficult to rewrite,
      may come in a separate commit.
    - `test_retry_fetching_encryption_info` Needs verification state
      changes. Will be rewritten on the event cache layer
2025-10-20 16:16:41 +02:00
Johannes Marbach 5f54237f4f feat(ffi): add bindings for logging in by generating a QR code on the new device
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-20 13:47:41 +02:00
Kévin Commaille f78f1795eb Upgrade Ruma
A new batch of breaking changes, allowing to stop providing dummy
`SupportedVersions` where they are not necessary.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-20 12:39:58 +02:00
Jorge Martin Espinosa dcd8aa13f0 fix: NotificationSettings::unmute_room didn't clear the cached notification mode 2025-10-17 12:02:38 +02:00
Ginger a4e68ba885 feat: Move Client::get_dm_room into the main impl Client block
This patch moves the `Client::get_dm_room` helper function and its tests
from `src/encryption/mod.rs` to `src/client/mod.rs`, so it may be used
without the `e2e-encryption` crate feature enabled.

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

Signed-off-by: Ginger <ginger@gingershaped.computer>
2025-10-16 14:03:09 +00:00
Johannes Marbach e8fb133cbf feat(oauth): Enable new devices to generate a QR code for login
This patch adds the complementary login flow for the already existing QR code login support.
Namely, previously it was only possible for the new device to scan a QR code to log in. Now
it's possible for the new device to create the QR code and let the existing device scan it.

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

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-15 16:04:50 +02:00
Damir Jelić a11daf24e5 test(ui): Test that the recency comparison function implements a total order 2025-10-14 17:12:41 +02:00
Damir Jelić b012512a21 chore(ui): Fix a copy/paste issue and add a note explaining a sort implementation
Co-authored-by: Benjamin Bouvier <benjamin@bouvier.cc>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-10-14 17:12:41 +02:00
Benjamin Bouvier 818b1b6000 test(event cache): rewrite the test_redact_touches_thread to make it resilient to races
In the previous version of the thread, the following sequence of events
could happen:

- we subscribe to the thread linked chunk changes
- *then*, the events are being added to the thread linked chunk

This is an edge case where, since we've subscribed to the thread linked
chunk, the thread root will be "known" to be part of a thread, and will
be appended to the thread linked chunk.

If the events happen in the other order (first the events are added to
the thread linked chunk, then we subscribe to the changes), then the
thread root will not be part of the thread linked chunk (because when it
arrived, we didn't know it would be a thread root). As such, the thread
linked chunk state would end up being different in this case.

The solution is to make it so that the thread linked chunk is always
subscribed to *before* any events are added to it. This way, we make
sure that we'll always have the thread root in the thread linked chunk.
2025-10-14 15:37:04 +02:00
Benjamin Bouvier 5b523d21e4 review: rename try_remove_event to remove_if_present 2025-10-14 15:37:04 +02:00
Benjamin Bouvier fcab05e44b chore: make clippy happy 2025-10-14 15:37:04 +02:00
Benjamin Bouvier 70fb53612d test(timeline): ensure that a thread summary being removed is properly propagated to the main timeline 2025-10-14 15:37:04 +02:00
Benjamin Bouvier 059b5e7c1f feat(timeline): correctly mark a replied-to event as redacted, in threads 2025-10-14 15:37:04 +02:00
Benjamin Bouvier 9d7c21f508 refactor(timeline): make it possible to pass an EmbeddedEvent to maybe_update_responses 2025-10-14 15:37:04 +02:00
Benjamin Bouvier 3705b73256 feat(event cache): when a thread has only redacted replies, remove the thread summary 2025-10-14 15:37:04 +02:00
Benjamin Bouvier 3fb874f901 fix(event cache): also update the thread summary when the redacted event is not the latest one 2025-10-14 15:37:04 +02:00
Benjamin Bouvier 215087f2c1 feat(event cache): have redaction affect thread chunks and summaries
In this initial version, a redaction will:

- *remove* the event from the thread chunk, as does Element Web,
- update the thread summary to reflect the new number of messages in the
  thread, and let us have a thread summary with 0 replies.

A next commit will adapt the code so that a thread summary with 0
replies is removed.
2025-10-14 15:37:04 +02:00
Kévin Commaille 1e2bf39a7c Update Ruma
Brings changes to the requests metadata. It was changed from a struct to a trait, and the authentication scheme is now an associated type.

This allows to forbid at compile time requests that use an unsupported authentication scheme.
2025-10-14 15:32:32 +02:00
vaw c1bc814ac2 feat(timeline): Use read receipt as fallback for read marker
Signed-off-by: vaw <git@nlih.de>
2025-10-14 09:07:50 +01:00
Richard van der Hoff 7185fcbac8 Merge pull request #5763 from matrix-org/rav/history_sharing_exclude_insecure_devices
crypto: Fix bugs in processing incoming encrypted to-device messages
2025-10-13 17:02:14 +01:00
Richard van der Hoff 01e2e4877c test(crypto): Regresion test for #5613
Add a test to ensure that history-sharing still works when "exclude insecure
devices" is enabled.
2025-10-13 16:41:56 +01:00
Richard van der Hoff 3622355a08 test(crypto): add regression test for #5768 2025-10-13 16:41:56 +01:00
Richard van der Hoff c388332e47 crypto: fall back to sender_device_keys for encrypted to-device messages
When receiving an encrypted to-device message, if the sender device is not in
the store, but the event includes `sender_device_keys`, use
`sender_device_keys` to do the verification checks etc.

Fixes: https://github.com/matrix-org/matrix-rust-sdk/issues/5768
2025-10-13 16:41:56 +01:00
dependabot[bot] 6042a9e9b0 chore(deps): bump crate-ci/typos from 1.37.2 to 1.38.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.37.2 to 1.38.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.37.2...v1.38.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 17:07:56 +02:00
dependabot[bot] 04260458ef chore(deps): bump qmaru/wasm-pack-action from 0.5.1 to 0.5.2
Bumps [qmaru/wasm-pack-action](https://github.com/qmaru/wasm-pack-action) from 0.5.1 to 0.5.2.
- [Release notes](https://github.com/qmaru/wasm-pack-action/releases)
- [Commits](https://github.com/qmaru/wasm-pack-action/compare/v0.5.1...v0.5.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 17:04:11 +02:00
Benjamin Bouvier 7c3a8b335a doc(timeline): tweak wording of TimelineBuilder::with_focus
It was incorrect to say that the timeline focus can be changed after the
timeline has been created, since it is *not* the case. Also explained
what the default value is.
2025-10-13 16:42:45 +02:00
Kévin Commaille a8aa8761d8 Add changelog for waveform changes
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-13 14:10:14 +02:00
Kévin Commaille bfc96181dd refactor(sdk): Change waveform to be a list of values between 0 and 1
Most clients will probably work with values between 0 and 1 and need to
convert it just to send it, so we can move that conversion into the SDK.

This is also more forwards-compatible, because MSC3246 now has a
different max value for the amplitude, so when this becomes stable, the
only change needed will be in the SDK.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-13 14:10:14 +02:00
Kévin Commaille eb1ee434b3 refactor(sdk): Allow to send waveform for any audio message
By moving the waveform declaration into `BaseAudioInfo`.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-13 14:10:14 +02:00
Richard van der Hoff e7fa8a429a test(crypto): simplify send_and_receive_encrypted_to_device_test_helper
No need to convert the event content to a to-device request, and then convert
back again.
2025-10-10 15:47:10 +01:00
Richard van der Hoff 8b6572bb23 test(crypto): Factor out test helper for encrypting to-device content
I'm going to need to suppress `sender_device_keys` for more tests, so pull out
a test helper to help with this.
2025-10-10 15:47:10 +01:00
Richard van der Hoff 43e94bcfb4 crypto: look up sender device for key bundles
Currently, when we receive a room key bundle to-device event, we don't look up
the sender device at all, meaning that the message is then marked as "from
missing device", which means that if you turn on "exclude insecure devices",
the message is dropped.

This patch changes the logic so that room key bundle to-device events are
treated the same way as most other to-device events (except room keys, which
continue to be special).

Fixes: https://github.com/matrix-org/matrix-rust-sdk/issues/5613, although the
integration test now fails because instead we hit https://github.com/matrix-org/matrix-rust-sdk/issues/5768.
2025-10-10 15:47:10 +01:00
Michael Goldenberg 588d604653 refactor(indexeddb): remove extraneous log message
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 90cf669f94 refactor(indexeddb): import transaction mode from indexed_db_futures rather than web_sys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 70a608b3b5 refactor(indexeddb): remove nested memory store from MediaStore impl
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg bcd4337985 test(indexeddb): add integration tests for MediaStoreInner
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 58972ca9d9 fix(indexeddb): ensure media that ignore retention policy is always put into IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg ef672271c2 fix(indexeddb): ensure tx is committed in MediaStore::set_media_retention_policy_inner
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 1bc417956c feat(indexeddb): add IndexedDB-backed impl for MediaStoreInner::set_ignore_media_retention_policy_inner
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 1127184db2 refactor(indexeddb): add transaction fn for putting media into IndexedDB
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 63a3c2d51a refactor(indexeddb): remove base64 encoding of unencrypted media content
This makes unencrypted content sizes consistent and testable.

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 3f55c217c1 feat(indexeddb): add IndexedDB-backed impl for MediaStoreInner::clean_inner
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 19632683f7 refactor(indexeddb): make getters for media content size key consistent with those for other media keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 03f964a5a3 refactor(indexeddb): add fns to get field components of indexed media keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 752beadb83 fix(indexeddb): add associated index to IndexedKey<Media> where missing
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 6244721ab4 refactor(indexeddb): add transaction fns for deleting media by content size and access time
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 6e034b0d7b refactor(indexeddb): add transaction fn for getting the size of the media cache
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg e4a717dff5 refactor(indexeddb): add media-specific transaction fns for getting and operating on keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 6acf628fc5 refactor(indexeddb): import cursor direction enum from indexed_db_futures rather than web_sys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg db91bb35ee refactor(indexeddb): add transaction fns for getting and operating on keys
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 3b7dbf5c04 feat(indexeddb): add IndexedDB-backed impl for MediaStoreInner::last_media_cleanup_time_inner
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg 22bfe8fbd3 refactor(indexeddb): add fns to get and put media cleanup time
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg e4243b7af3 refactor(indexeddb): add indexed type and traits for media cleanup time
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg f891c1ca06 refactor(indexeddb): add type for representing media cleanup time
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Michael Goldenberg c2839d7594 refactor(indexeddb): add conversions and operations for UnixTime
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-10 10:09:52 +02:00
Richard van der Hoff c45ede972e crypto: factor out Account::get_event_sender_device
`Account::parse_decrypted_to_device_event` is getting a bit big and unwieldy,
so factor out the bit that attempts to find the sending device.

(Also, remove an outdated TODO.)
2025-10-09 18:26:46 +01:00
Johannes Marbach 9b485013e1 feat(ffi): add bindings for listening to room send queue updates
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-09 15:18:04 +01:00
Kévin Commaille cb3d281f8f Upgrade Ruma after removal of legacy mention push rules
The legacy mention push rules were removed, and the
`contains_display_name` condition was deprecated.

Some tests check for backwards-compatibility with legacy mentions, so we
need to add them back for those tests.

A test with an encrypted event was relying on the legacy mentions, so
the encrypted event was replaced with another one with an intentional
mention.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-09 15:08:38 +01:00
Kévin Commaille a72c19a240 test(ui): Allow to set own user id of TestRoomDataProvider
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-09 15:08:38 +01:00
Kévin Commaille cf4a1dee4b Upgrade Ruma after StringEnum changes
StringEnum now also implements Ord, PartialOrd, Eq and PartialEq so it
is not necessary to derive them. Also the ordering used is comparing the
string representation of the variants.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-09 15:08:38 +01:00
Kévin Commaille 487470be8f Upgrade Ruma after extended profile field stabilization
Extended profile fields were stabilized so the old endpoints are now
deprecated, and there are a few other changes.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-09 15:08:38 +01:00
Richard van der Hoff b94823216d Merge pull request #5766 from matrix-org/rav/device_keys_self_signature
crypto: give `DeviceKeys` ability to check their own signature
2025-10-09 12:26:41 +01:00
Richard van der Hoff 232119cf57 crypto: avoid redundant conversion to DeviceData
There is (now) no need to turn the `sender_device_keys` into a `DeviceData`.
2025-10-09 11:32:09 +01:00
Richard van der Hoff 64818e2ef9 crypto: docs on Account::check_sender_device_keys (#5765)
I had to do some thinking about this, so wrote down my conclusions.
2025-10-09 11:31:30 +01:00
Richard van der Hoff a2ded93234 crypto: give DeviceKeys ability to check their own signature
Adds `DeviceKeys::has_signed` and `DeviceKeys::check_self_signature`, and
removes `DeviceData::has_signed` and `DeviceData::verify_device_keys`.

I just found this easier to grok, and it means we can avoid needlessly turning
a `DeviceKeys` into a `DeviceData` sometimes.
2025-10-09 11:14:42 +01:00
Stefan Ceriu fc892564d8 fix(spaces): handle empty string room names when computing the display names
Fixes #5762
2025-10-09 12:59:06 +03:00
Johannes Marbach 358803783f feat(oauth): add LoginProgress::SyncingSecrets
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-08 11:00:54 +02:00
Benjamin Bouvier ef440eed2b chore(base): don't log the same missing room info log line on every single sync
This should only happen when a room has been forgotten and was a room
DMs before. Ideally, we'd clean up the room DM event data, but since
this is slightly more involved, we don't do that here just quite yet.
2025-10-07 21:03:09 +02:00
Johannes Marbach 79e1930b22 Make LoginProgres::EstablishingSecureChannel generic in order to reuse it for the other QR login flow
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-07 21:00:42 +02:00
Kévin Commaille 6191e2c24e fix(sdk): Make impl Stream return type not use any lifetime
With Rust 2024, by default `impl` return types use any generic that is
in scope, so in these cases the lifetime of `self`.

But since the return type is actually owned, the returned impl shouldn't
use any lifetime, which is what `use<>` does.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-07 15:22:52 +02:00
Kévin Commaille ba2fe1d387 fix(crypto): Make impl Stream return type not use any lifetime
With Rust 2024, by default `impl` return types use any generic that is
in scope, so in these cases the lifetime of `self`.

But since the return type is actually owned, the returned impl shouldn't
use any lifetime, which is what `use<>` does.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-07 15:22:52 +02:00
Stefan Ceriu c7b4b5dc05 chore(ffi): expose the computed SpaceRooms display name
This reuses the same naming scheme used in the FFI Room and RoomInfo
2025-10-07 15:33:36 +03:00
Stefan Ceriu 87d9bd14e3 feat(spaces): reuse existing room display name computation logic for spaces 2025-10-07 13:05:59 +03:00
Benjamin Bouvier 44a4ca94be chore: fix new typos 2025-10-06 17:39:23 +02:00
dependabot[bot] 3b43a7e5e8 chore(deps): bump crate-ci/typos from 1.36.3 to 1.37.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.36.3 to 1.37.2.
- [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.36.3...v1.37.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 17:39:23 +02:00
dependabot[bot] 773d304f9e chore(deps): bump CodSpeedHQ/action from 4.0.1 to 4.1.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.0.1 to 4.1.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/653fdc30e6c40ffd9739e40c8a0576f4f4523ca1...6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 17:19:31 +02:00
dependabot[bot] b47174e394 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from 7c2dc36a6fe4a75848d9397e34c95474f38c82ef to 744a6d5e0db5d189ad36edb08c5f77107cc42310.
- [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/7c2dc36a6fe4a75848d9397e34c95474f38c82ef...744a6d5e0db5d189ad36edb08c5f77107cc42310)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-06 17:11:40 +02:00
Damir Jelić b1e0339159 ci: Add the crypto team as CODEOWNERS for the indexeddb crypto store implementation 2025-10-06 14:58:37 +02:00
Benjamin Bouvier 7fbc4144b1 feat(timeline): allow a poll edit to be an embedded event
This will properly show edited polls as the latest thread id, as a nice
benefit.
2025-10-06 14:05:34 +02:00
Benjamin Bouvier a2a26ae45e refactor(timeline): pass directly the poll start and fallback text to PollState ctor 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 06301bc2f8 refactor(timeline): store fewer datum a in PollState
We don't really need the poll start event entirely, since we're only
interested in the start block and the fallback text. With this, it'll be
simpler to create embedded polls from poll edit events.
2025-10-06 14:05:34 +02:00
Benjamin Bouvier 85abe76121 feat(timeline): also support poll edits as embedded events 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 97ff61081a feat(timeline): support edits in embedded events 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 0017ccb0c1 feat(event cache): update a thread summary if an edit related to the latest thread reply 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 350fdd8ad4 feat(common): add support for extracting only an edited target event id from an edit event 2025-10-06 14:05:34 +02:00
Benjamin Bouvier f937bf60e2 refactor(event cache): make naming more consistent around latest thread event 2025-10-06 14:05:34 +02:00
Benjamin Bouvier da394f5015 refactor(event cache): update the thread summary in a separate function 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 158e3925b7 refactor(event cache): don't collect back-paginated events that are going to be filtered out immediately 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 4828f4c555 refactor(event cache): rename save_event to save_events as it's involving multiple events 2025-10-06 14:05:34 +02:00
Benjamin Bouvier 92e7cb3af2 refactor(event cache): save the content of threaded events in the store 2025-10-06 14:05:34 +02:00
Benjamin Bouvier a6a590aa1f test(timeline): add a test that an edit to a thread's latest event updates the thread summary 2025-10-06 14:05:34 +02:00
Kévin Commaille d01a28c9b2 Upgrade Ruma
Brings a breaking change with event structs being non-exhaustive now,
so they need to be constructed with methods rather than with a struct
declaration.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-06 10:21:00 +02:00
Johannes Marbach 68075b65fb refactor(auth): make auxiliary functions reusable outside LoginWithQrCode
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-10-04 09:41:16 +02:00
Kévin Commaille d4d40945e8 refactor(tests): Use EventFactory to build events
There is a breaking change in Ruma and those types are now
non-exhaustive so they can't be built with the struct declaration
anymore.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 17:01:58 +02:00
Kévin Commaille 2b69a7f741 feat(sdk-test): Add conversions to deserialized types for EventBuilder
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 17:01:58 +02:00
Stefan Ceriu d85b45ed64 feat(spaces): sort space room list rooms as defined in the spec
The ordering criteria is defined at https://spec.matrix.org/latest/client-server-api/#ordering-of-children-within-a-space. The gist is that `order` comes first, then `timestamp` and finally the `room_id`

This is not available for top level spaces, but there is an MSC that addresses it at https://github.com/matrix-org/matrix-spec-proposals/pull/3230 and Ruma support has been added in https://github.com/ruma/ruma/pull/2231. The SDK side implementation for that will come in a later PR.
2025-10-03 15:22:52 +02:00
Stefan Ceriu b43237536d chore(spaces): store the full children_state data when fetching /hierarchy 2025-10-03 15:22:52 +02:00
Kévin Commaille fbafae42bb refactor(tests): Replace uses of EventBuilder::into_raw
We actually want other event formats in those cases, and in most cases
just using `.into()` is enough to generate the proper format.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 15:22:25 +02:00
Kévin Commaille 08563d4096 refactor(sdk-test): Use enum to represent possible event formats of EventBuilder
And use the proper fields for these formats. We also add more conversion
implementations for the types associated with these formats.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 15:22:25 +02:00
Andy Balaam 32f3670aeb task(crypto): Warn API users to fetch device info before processing verification requests 2025-10-03 14:21:44 +01:00
Ivan Enderlin 8a23aae9dc perf(common): Compute the message in TracingTimer when required (#5662)
This patch moves the creation of the `message` in `TracingTimer` if and
only if the log is enabled. Computing it every time is useless, and can
even slow things down (because of the time calculation).
2025-10-03 15:19:47 +02:00
Jorge Martín 2822815384 feat(ffi): Add NotificationSettings::get_raw_push_rules
This allows clients to get the raw push rules so they can be added to bug reports if needed
2025-10-03 15:16:07 +02:00
Ivan Enderlin a7cb094aaf feat(sqlite): Add a write-only connection in SqliteStateStore.
This patch introduces a write-only connection in `SqliteStateStore`
_à la_ `SqliteEventCacheStore`. The idea is to get many read-only
connections, and a single write-only connections 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.
2025-10-03 15:00:37 +02:00
Ivan Enderlin 764a8a4c77 doc(sqlite): Fix // to ///.
This patch transforms an inline comment into a doc comment.
2025-10-03 15:00:37 +02:00
mgoldenberg 2e6790d0a5 IndexedDB: upgrade indexed_db_futures dependency (#5722)
**NOTE:** _this should not be merged until matrix-org/rust-indexed-db#1
is merged! The `[patch]` in this branch should point to the official
`matrix-org` fork of `rust-indexed_db`, but is currently pointed at my
personal fork._

## Background

This pull request makes updates
[`indexed_db_futures`](https://docs.rs/indexed_db_futures/latest/indexed_db_futures/index.html)
in the `matrix-sdk-indexeddb` crate. The reason we'd like to update this
dependency is because the version currently used does not fully support
the Chrome browser (see #5420).

The latest version of `indexed_db_futures` has significant changes. Many
of these changes can be integrated without issue. There is, however, a
single change which is incompatible with the `matrix-sdk-indexeddb`
crate. Namely, one cannot access the active transaction in the callback
to update the database (for details, see Alorel/rust-indexed-db#66).

### An Updated Proposal

Originally, new migrations were implemented in order to work around this
issue (see #5467). However, the proposal was ultimately rejected (see
@andybalaam's
[comment](https://github.com/matrix-org/matrix-rust-sdk/pull/5467#issuecomment-3149550617)).

For this reason, the dependency has instead been `[patch]`ed in the
top-level `Cargo.toml` with a modified version of `indexed_db_futures`
(see matrix-org/rust-indexed-db#1). Furthermore, these changes have been
proposed to the maintainer and are awaiting feedback (see
Alorel/rust-indexed-db#72).

### Why do we need the active transaction in our migrations?

The `crypto_store` module provides access to the active transaction to
its migrations (see
[here](https://github.com/matrix-org/matrix-rust-sdk/blob/ca89700dfe9f29dcd823bb10861807f9d75e0634/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs#L211)).
Furthermore, there is a single migration (`v11_to_v12`) in the
`crypto_store` module which actually makes use of the active transaction
(see
[here](https://github.com/matrix-org/matrix-rust-sdk/blob/ca89700dfe9f29dcd823bb10861807f9d75e0634/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v11_to_v12.rs#L23)).

For clarity, the reason `v11_to_v12` is problematic in the latest
versions of `indexed_db_futures` is because it is simply adding an index
to an object store which was created in a different migration and this
requires access to the active transaction. All the other migrations
create object stores and indices in the same migration, which does not
suffer from the same issue.

## Changes

- Move `indexed_db_futures` to the workspace `Cargo.toml` and add a
`[patch]` so that it points to a modified version.
- Add `GenericError` type and conversions in order to more easily map
`indexed_db_futures` errors into `matrix-sdk-*` errors.
- Update all IndexedDB interactions so that they use the upgraded
interface provided by `indexed_db_futures`
- Add functionality for running `wasm-pack` tests against Chrome


---
Closes #5420.

---

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


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

---------

Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-10-03 14:10:10 +02:00
Stefan Ceriu 67d8db3d93 fix(spaces): filter out non-joined rooms from the space leaving process and handle 2025-10-03 13:43:14 +03:00
Ivan Enderlin 52518e0e2e fix(sdk): Use RoomPowerLevels::user_can_kick_user in filter_any_sync_state_event.
This patch replaces `user_can_kick` by `user_can_kick`: it performs an
extra check to make sure the acting user has at least the same power
level as the target user.
2025-10-03 12:37:37 +02:00
Kévin Commaille b8b54246c4 Silence unused-imports lint
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 12:27:50 +03:00
Kévin Commaille 95e93ca00b fix(ui): Fix broken links
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 12:27:50 +03:00
Kévin Commaille bb6ba08dfb fix: Remove newly detected unused imports
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 12:27:50 +03:00
Kévin Commaille 8c515b0c12 fix(docs): Replace doc_auto_cfg with doc_cfg feature
The former has been merge in the latter, and it errors when generating
the docs in a recent version of nightly, like the one used on docs.rs.

This also requires to bump the version of nightly used in CI, otherwise
it would break the docs generation.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-03 12:27:50 +03:00
Ivan Enderlin 81a69f82d2 feat(sdk): Accept invite for latest event if it targets the current user. 2025-10-03 11:26:45 +02:00
Ivan Enderlin f4a6d12979 refactor(sdk): Split power_levels in latest_event.
This patch splits `power_levels: &Option<(&UserId, RoomPowerLevels)>`
into 2 variables: `own_user_id: Option<&UserId>` and `power_levels:
Option<&RoomPowerLevels>`. The idea is to be able to get the
`own_user_id` even if the power levels are `None`.
2025-10-03 11:26:45 +02:00
Ivan Enderlin 203a3783ae feat(sdk): Support m.room.membership with membership: "invite" as latest event. 2025-10-03 11:26:45 +02:00
Ivan Enderlin 8eb7264e5d test(sdk): Test that m.room.member for an invite can be a latest event candidate. 2025-10-03 11:26:45 +02:00
Kévin Commaille a4bd36cbe8 fix(ci): Fix cargo-codspeed command
A new release occurred which has a breaking change in the syntax used to
select a benchmark.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-02 21:33:24 +02:00
Stefan Ceriu 8e8ad0167a change(spaces): return a reference to the rooms vector from the leave handle
well now
2025-10-02 12:41:42 +03:00
Stefan Ceriu 0f78959c9a change(spaces): compute LeaveSpaceRooms for the LeaveSpaceHandle asynchronously in its constructor 2025-10-02 12:41:42 +03:00
Stefan Ceriu 7a431a3afd change(spaces): have the leave space rooms interface take a filter
This helps make sure the rooms to be left were actually part of the space graph as they are stored inside the `LeaveRoomHandle` and filtered from there. On the FFI layer on the other hand, we still take plain strings as working around the limitations would've significantly complicated things.
2025-10-02 12:41:42 +03:00
Stefan Ceriu a6d033ea4c chore(spaces): move joined_rooms and the SpaceGraph underneath the same Arc Mutex 2025-10-02 12:41:42 +03:00
Stefan Ceriu ad41cbc368 chore(spaces): remove unused Unknown Space Error variant 2025-10-02 12:41:42 +03:00
Stefan Ceriu cf0c3e7009 chore(spaces): move the LeaveSpaceRoom struct to the top of the file 2025-10-02 12:41:42 +03:00
Stefan Ceriu 3a60d34f3f feat(ffi): expose the space service leaving interfaces
fix newline ffi
2025-10-02 12:41:42 +03:00
Stefan Ceriu 9114c22b70 feat(spaces): add mechanism for _ordely_ leaving a space and its children
When leaving a space the user should be informed of which rooms are DMs (already part of the `SpaceRoom`)
and in which they might be the last admin, where leaving would prevent anybody else for taking control.
2025-10-02 12:41:42 +03:00
Stefan Ceriu 8655afd117 chore(spaces): store the built space graph in between the various updates so it can be used for leaving spaces 2025-10-02 12:41:42 +03:00
Stefan Ceriu f5ec9b6427 feat(spaces): add graph method for retrieving a flat list of nodes belonging to a subtree ordered in bottom up dfs visiting order. 2025-10-02 12:41:42 +03:00
Ivan Enderlin 2eab7cf818 fix(ui): RoomListItem refreshes its cache_is_space. 2025-10-02 09:45:14 +02:00
Kévin Commaille 6072618e85 Add changelog for caption change
Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-02 08:15:56 +02:00
Kévin Commaille 70b19cc907 refactor(sdk): Use TextMessageEventContent to send a caption
It doesn't make sense to send a formatted caption without a plain text
caption so using TextMessageEventContent forces the latter to be present.

This also allows to use the helpful constructors of
TextMessageEventContent.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-10-02 08:15:56 +02:00
Hubert Chathi 57d21ccdf6 Create a separate error variant to indicate a failure importing a secret. (#5647)
Part of the fix to
https://github.com/element-hq/element-x-android/issues/5099

Allows applications to distinguish between errors that occur when
unlocking Secret Storage, or errors that occur when importing a secret,
so that they can display appropriate feedback (or not) to the user.

- [ ] 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: Hubert Chathi <hubertc@matrix.org>
2025-10-02 08:14:46 +02:00
Benjamin Bouvier 37ee5d5075 refactor(stores): get rid of the temporary compute_filter_strings now that Ruma has been updated
This was a local fix for a bug in Ruma, that has been fixed upstream since then, so we can get rid of the workaround now.
2025-10-01 16:50:23 +00:00
Benjamin Bouvier 681b22142f refactor(timeline): add more logs when we couldn't create an embedded event
This should help figuring out why some thread's latest replies are
marked as "unsupported events".
2025-10-01 10:54:00 +02:00
Benjamin Bouvier 248d77a4d9 refactor(ffi): add debug logging when a latest event is not a standalone content item 2025-10-01 10:54:00 +02:00
Benjamin Bouvier f4451b5c82 refactor(timeline): TimelineAction::from_content always returns Something now 2025-10-01 10:54:00 +02:00
Mathieu Velten 59b7da247c Add some doc to add_event_handler for invites and stripped state (#5705)
Signed-off-by: Mathieu Velten <mathieu@velten.xyz>
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2025-09-30 14:25:17 +00:00
Benjamin Bouvier be5bd449b5 test(timeline): ensure unthreaded receipts are loaded in the main timeline view mode 2025-09-30 15:09:57 +02:00
Benjamin Bouvier 2eb29518dc refactor(timeline): no need to look at the receipt timestamp
I was wrong in a previous commit: both receipts are on the same event
anyways, so we can safely override the keys in the read receipts map
(overriding would mean both receipts point to the same event, which is
fine, as we're displaying only one of those).
2025-09-30 15:09:57 +02:00
Benjamin Bouvier 16d0840115 refactor(timeline): move code around for loading initial main|unthreaded receipts 2025-09-30 15:09:57 +02:00
Benjamin Bouvier d90576bf0d fix(timeline): when loading initial receipts for main|unthreaded, load both kinds
This is a fix, because some other code elsewhere will use both kinds of
receipts whenever they're received over sync. The code that's modified
in this patch is called for the initial load of receipts, that happens
whenever we see a new event. Since the two code paths were not doing the
same thing, this would affect the displayed receipts, depending on
whether we received them during sync, or after loading the timeline for
the first time.
2025-09-30 15:09:57 +02:00
Benjamin Bouvier bbeb2d21b1 refactor(timeline): slightly rearrange code so as to remove a dubious comment 2025-09-30 14:29:27 +02:00
Benjamin Bouvier 187b646c07 refactor(event cache): have the room pagination handle waiting for the previous pagination token from sync
This case is very specific to the room pagination, and will not apply to
the thread pagination; by removing it from the generic pagination logic,
we'll be able to use the generic pagination logic for threads.
2025-09-30 14:29:27 +02:00
Benjamin Bouvier 8a47e3cd1c refactor(event cache): inline conclude_load_more_for_fully_loaded_chunk into its only caller
And that's one overlong function name less!
2025-09-30 14:29:27 +02:00
Benjamin Bouvier 973d71f54e docs(event cache): add comments to clarify when None can be returned from internal pagination methods 2025-09-30 14:29:27 +02:00
dependabot[bot] 943b048fa0 chore(deps): bump bnjbvr/cargo-machete
Bumps [bnjbvr/cargo-machete](https://github.com/bnjbvr/cargo-machete) from cb0995971182a3babbea3f086bf306d5509cac47 to 7c2dc36a6fe4a75848d9397e34c95474f38c82ef.
- [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/cb0995971182a3babbea3f086bf306d5509cac47...7c2dc36a6fe4a75848d9397e34c95474f38c82ef)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 09:36:57 +01:00
dependabot[bot] 2c70c31c56 chore(deps): bump crate-ci/typos from 1.36.2 to 1.36.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.36.2 to 1.36.3.
- [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.36.2...v1.36.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 07:36:48 +03:00
Ivan Enderlin be7129bacc chore(ui): Remove an unnecessary Arc in SpaceRoomList.
`SharedObservable` is already shareable, no need for an `Arc` here.
2025-09-29 09:54:54 +03:00
Ivan Enderlin 2ec33183c4 doc(sqlite): Fix formatting and typo. 2025-09-26 16:07:20 +02:00
Doug d6d720c015 ffi: Expose a room list filter for spaces. 2025-09-26 10:22:53 +01:00
Stefan Ceriu 5b52c729a5 feat(spaces): automatically subscribe to SpaceRoomList "parent" space room info updates (#5712)
… when known to the client and forward updates through the existing
`subscribe_to_space_updates` mechanisms.

This allows clients to listen to updates without having to resort to a
separate room info subscription on their side.
2025-09-25 13:05:29 +03:00
Benjamin Bouvier 7d649e92d4 fix(timeline): don't listen to live thread events when the timeline is focused on a thread permalink 2025-09-25 11:27:18 +02:00
Benjamin Bouvier 021d3fb5d7 test(timeline): add a test to show that threaded permalink timeline receives thread live updates 2025-09-25 11:27:18 +02:00
Jorge Martín 5f02212312 fix(ffi): ffi::Room::load_or_fetch_event fails with missing room_id
The raw event was being deserialized in a wrong way and it could have a missing room id in some cases, returning an error even when the event was found
2025-09-25 10:57:48 +02:00
Benjamin Bouvier e158e8abc0 refactor(timeline): in thread permalinks, avoid back-paginating if the root event is part of the /context response
For thread permalinks, we start with a /context query that will load the
focused event, and maybe a few other in-thread events. In fact, it can
also include the thread root event, which was excluded before. Instead,
we would get a previous-token for back-paginations, which would be used
in /relations. When the request to /relations returns an empty previous
token, that means we've reached the start of the thread, and in this
case we would manually load the root with /event.

We can do better, if the root event is part of the initial /context
response: skip the back-paginations altogether, and make sure to include
the root event in `init_focus()`.
2025-09-25 10:21:19 +02:00
Damir Jelić e2ec8bcbd6 ci: Install clippy for the test-crypto CI run 2025-09-25 10:14:21 +02:00
Benjamin Bouvier 36e0d4bfb8 ci: don't compile the benchmarks in a separate CI step
They're built as part of the codspeed run these days, so this is
duplicated wasteful work.
2025-09-25 10:05:27 +02:00
Benjamin Bouvier 05362be89a ci: use the latest bnjbvr/cargo-machete action
It's much faster now as it downloads the latest version of the
precompiled binary from Github, and it will use the latest tagged
version of cargo-machete by default.
2025-09-25 10:05:27 +02:00
Ivan Enderlin 03fc5dacbe feat(ui): The recency sorter handles recency stamp _and_ latest event's timestamp.
This patch revisits a feature we have disabled a couple of days ago:
the `recency` sorter was initially only supporting the recency stamp,
then later the recency stamp _and_ the latest event's timestamp. It was
however buggy and we had to revert it. Now it's time to re-introduce it
but with a different approach.

The previous rules were:

1. if two rooms have a latest event, use their latest event's timestamps
   as their _scores,
2. if one of room has a latest event, use the recency stamp as their
   _scores_ for both rooms.

Rule 2 was buggy because one room was sometimes using its latest
event's timestamp, and sometimes its recency stamp, based on what it was
compared to. It was an error!

The new rules are the following:

1. unchanged
2. if one room has a latest event, use its latest event's timestamp as
   its _score_, and use no _score_ for the other room,
3. if two rooms have NO latest event, use the recency stamp as their
   _scores_.

It means that a room with no latest event will always be sorted _after_
a room with a latest event. It can feel cruel, but it should be an edge
case. When a room is synchronised, it should receive events, which
should trigger the computation of a latest event.

Note that this patch also renames _rank_ to _score_, as I consider it's
a better vocabulary. It could be confusing to use _rank_ as one can
expect all rooms to be indexed and get a rank, but it's not the case.
_Score_ sounds better.
2025-09-24 17:51:38 +02:00
Alexis Loiseau 0a0e31af83 feat(ui): add custom events to timeline when explicitly filtered
This allows custom message-like events (created by the `EventContent` macro from ruma) to be added to the timeline if they are explicitly allowed when building the timeline with a custom `event_filter`.

The custom event content is not available directly to the consumer, but it can still fetch it from the matrix-sdk client with its `event_id`, or display a "this type of event is not supported". 

Signed-off-by: Itess <me@aloiseau.com>

Fixes #5598.
2025-09-24 15:50:50 +00:00
Ivan Enderlin 290f27a343 doc(base): Update CHANGELOG.md. 2025-09-24 16:12:09 +02:00
Ivan Enderlin 0033de1f49 refactor(base): Rename sync_lock to state_store_lock.
This patch renames the `sync_lock` to `state_store_lock` because it is
what it is. It's not about the sync, it's about the state store.
2025-09-24 16:12:09 +02:00
Ivan Enderlin 688eb6880d refactor(sdk): Move the RoomLatestEvents* types in their own module.
This patch moves the `RoomLatestEvents*` types in their own new
`room_latest_events` module.
2025-09-24 14:26:14 +02:00
Ivan Enderlin 07704c7835 feat(sdk): Put locks around RoomLatestEvents.
This patch tries to solve a problem raised by Complement Crypto. In
`compute_latest_events`, in two places, `RegisteredRooms::rooms` was
locked with an exclusive write access. Then, during the updates of
the latest events (via `RoomLatestEvents::update_with_event_cache` and
`RoomLatestEvents::update_with_send_queue`), the state store lock is
acquired to update the state store. At the same time, in the sync, the
state store lock can **already** be taken to store the new updates, and
the function `subscribe_to_room_latest_events` is called, which waits
on the lock around `RegisteredRooms::rooms` to be available. We have a
dead lock:

- `compute_latest_events` waits on the state store lock while the lock
  on `RegisteredRooms::rooms` is taken with an exclusive access,
- the sync has acquired the state store lock while it waits on the lock
  on `RegisteredRooms::rooms`

This patch introduces a lock inside `RoomLatestEvents`. A new
`RoomLatestEventsState` type is introduced to be the lockable value. A
new `RoomLatestEvents::read()` and `RoomLatestEvents::write()` methods
are introduced to respectively return a `RoomLatestEventsReadGuard` and
a `RoomLatestEventsWriteGuard` type. The idea is to abstract a bit the
owned lock guard and to distribute the methods that were previously on
`RoomLatestEvents` in the new guard types, so that we keep the `&self`
and `&mut self` semantics, plus we take a lock for multiple operations.

The deadlock is fixed because of all the following reasons combined:

- only `RegisteredRooms::room_latest_event`,
  `RegisteredRooms::forget_room` and `RegisteredRooms::forget_thread`
  take a write lock over `RegisteredRooms::rooms`,
- `compute_latest_events` no longer takes a write lock over
  `RegisteredRoooms::rooms`, but it also has a short-lived lock:
  its read lock is dropped as soon as the **owned** write lock over
  `RoomLatestEvents` is taken.

Now, `subscribe_to_room_latest_events` can acquire a write lock over
`RegisteredRooms::rooms` while `compute_latest_events` is doing the
updates. If the state store lock is acquired here, it will just wait
its availability: the sync flow can finish and release the lock without
being blocked by `subscribe_to_room_latest_events`.
2025-09-24 14:26:14 +02:00
Ivan Enderlin 60c3b3dd43 chore(base): Scope the lock guard to a block.
This patch ensures that the state store lock guard (`_sync_lock`) is
scoped to a block so that it cannot live too long.
2025-09-24 14:26:14 +02:00
Ivan Enderlin d00cfb0ba8 fix(base): Take the state store lock before updating it.
This patch updates
`BaseClient::receive_sync_response_with_requested_required_states` to
take the state store lock before applying any change onto it.
2025-09-24 14:26:14 +02:00
Ivan Enderlin f7e1866bda feat(sdk,ui): Automatically subscribe to LatestEvents if EventCache has subscribed. 2025-09-24 14:26:14 +02:00
Ivan Enderlin b316c534ea feat(sdk): Call LatestEvents::listen_to_room for rooms in sync response.
This patch updates `SlidingSyncResponseProcessor::handle_room_response`
to automatically call `LatestEvents::listen_to_room` based on
`http::Response`'s `rooms`.

Why? Because when a sync is received, we want its `LatestEventValue`
to be computed, so that it can trigger a `RoomInfoNotableUpdate`,
which will update the `RoomList`, which will re-sort the
rooms. So far, `LatestEvents::listen_to_room` was called by
`RoomListService::subscribe_to_rooms`, but it's possible to receive a
room update via the sync for a room that is not subscribed, i.e. out
of the viewport of the room list in Matrix clients (it's recommended
to subscribe to rooms that “enter” the viewport). Without this
patch, rooms are receiving updates but the room list is not entirely
refreshed/recalculated.
2025-09-24 14:26:14 +02:00
multi prise fa7fd5df42 Remove unusual import 2025-09-24 12:35:17 +02:00
multi prise adc8276162 Add key opening logic to the media store 2025-09-24 12:35:17 +02:00
multi prise abecb33e34 Lint code 2025-09-24 12:35:17 +02:00
multi prise b26ce417f0 Add comments documenting the new structure 2025-09-24 12:35:17 +02:00
multi prise 5a1bd54bb1 Implement use of Zeroizing struct for string 2025-09-24 12:35:17 +02:00
multi prise 0d0e2aa472 Add ZeroiseOnDrop trait to secret and make the key a Box 2025-09-24 12:35:17 +02:00
multi prise 88ed0afcb3 Replace missing line 2025-09-24 12:35:17 +02:00
multi prise 9938ab8b1f Reimplement previous tests for the store and on top of the one testing the opening with a key 2025-09-24 12:35:17 +02:00
multi prise eed7384934 Remove some superfluous change 2025-09-24 12:35:17 +02:00
multi prise 32255cd178 Update changelog 2025-09-24 12:35:17 +02:00
multi prise c51536a054 reformat 2025-09-24 12:35:17 +02:00
multi prise 6099928b40 Remove conditional logic for running tests 2025-09-24 12:35:17 +02:00
multi prise fac1f295b2 Correct wrong borrow 2025-09-24 12:35:17 +02:00
multi prise 24d02a72e3 implement zeroizing of secrets after use 2025-09-24 12:35:17 +02:00
multi prise 2a073043fd Revert "Use of lifetime in order to not clone/copy the data"
This reverts commit 009ee3a0e5fdff1332aaf0b1e62ab2577d728b82.
2025-09-24 12:35:17 +02:00
multi prise c5b35209b3 Revert "Update matrix-sdk with new lifetimes"
This reverts commit 3a8f5f110cd755158e3a5605a282556da6060417.
2025-09-24 12:35:17 +02:00
multi prise 8e759befd3 Refactorize tests config to correspond with the new api 2025-09-24 12:35:17 +02:00
multi prise 9faffa5b10 Temporary comment insecure function 2025-09-24 12:35:17 +02:00
multi prise 31200357a0 Update matrix-sdk with new lifetimes 2025-09-24 12:35:17 +02:00
multi prise 75b8c9fe93 Use of lifetime in order to not clone/copy the data 2025-09-24 12:35:17 +02:00
multi prise 004d98230c Uncomment the config directives and allows test to run faster by usinf an insecure export function 2025-09-24 12:35:17 +02:00
multi prise eb37a0d2e1 Update some expect text to make sure they reflect the use of secret instead of a only a key to encrypt a store 2025-09-24 12:35:17 +02:00
multi prise 8a0e61e95b correct comment on the state store file 2025-09-24 12:35:17 +02:00
multi prise a2e2765298 correct comment on the crypto store file 2025-09-24 12:35:17 +02:00
multi prise 6e1e0981b1 remove typo 2025-09-24 12:35:17 +02:00
multi prise b2120a8f3d Update changelog to represent changes 2025-09-24 12:35:17 +02:00
multi prise 5d169ae765 Comment the use 2025-09-24 12:35:17 +02:00
multi prise 34dd7ea3cd Revert get_or_create_store_cypher to use 2025-09-24 12:35:17 +02:00
multi prise 4930c589a8 Refactorize SqliteStoreConfig::key and SqliteStoreConfig::passphrase method 2025-09-24 12:35:17 +02:00
multi prise f6d2e73cab More reformarting of files 2025-09-24 12:35:17 +02:00
multi prise 2bd5ec30d1 Correct some tests 2025-09-24 12:35:17 +02:00
multi prise 425b502977 Format files 2025-09-24 12:35:17 +02:00
multi prise 0dfecd78d6 Updated the store encryption to use a enum Secret instead of passphrase 2025-09-24 12:35:17 +02:00
multi prise 4754ac2cbf Updated changelog 2025-09-24 12:35:17 +02:00
multi prise 72bb452b5b Remove all passphrase mention 2025-09-24 12:35:17 +02:00
multi prise c17dbf9ebe Replace the passphrase logic with a key logic in the implementation of encrypted store 2025-09-24 12:35:17 +02:00
multi prise 5fd7c9e179 Add the key logic to the SqliteStoreConfig struct 2025-09-24 12:35:17 +02:00
Hubert Chathi 840ce43fed Add function to check if the user has another device to verify against (#5699)
Part of the fix for
https://github.com/element-hq/element-x-android/issues/4864 and
https://github.com/element-hq/element-x-ios/issues/4190

Allows applications to determine whether the user can verify against
another device in order to cross-sign their new device.
2025-09-24 11:31:42 +01:00
Benjamin Bouvier e71d565346 test: add a test for is_threaded when the focused timeline points to an in-thread event 2025-09-23 17:43:22 +02:00
Benjamin Bouvier 5a06f5f351 test: use a RoomContextResponseTemplate builder pattern to create responses to /context 2025-09-23 17:43:22 +02:00
Benjamin Bouvier 1e01e3fc62 refactor(timeline): don't allocate a vector for the in-thread events when initially loading a thread permalink 2025-09-23 17:43:22 +02:00
Jorge Martín bcba5f4571 chore(doc): Add changelogs 2025-09-23 14:13:34 +02:00
Jorge Martín 7736b50c04 fix(ui): Fix tests again.
The wrong pair of 'from' tokens were used for the forward paginations in the unit test
2025-09-23 14:13:34 +02:00
Jorge Martín 6e1dc121a5 fix(sdk+ui): Expose TimelineController::focus so the right backwards pagination case is used for focused thread pagination
Before, the same pagination as for the live thread timeline was used by mistake.

Fix the tests and check the right tokens are used for `/relations`.
2025-09-23 14:13:34 +02:00
Jorge Martín 08f0200174 refactor(sdk): Use .expect to unwrap the AnyPaginator, remove PaginationError::NotInstantiated
Also, improve the legibility of some usages
2025-09-23 14:13:34 +02:00
Jorge Martín eda561e00e refactor(ffi): Replace derefs and add doc comment to thread_root_event_id 2025-09-23 14:13:34 +02:00
Jorge Martín 578320cefc fix(sdk): Make sure we only include the events received from the /context request that are part of the thread in the case where the event focus is for a threaded event
Modify the tests and mocks so this filtering is checked.
2025-09-23 14:13:34 +02:00
Jorge Martín 59ed28d3f8 refactor(sdk): add several helper functions to the AnyPaginator wrapper, use them where needed
Move `hide_threaded_events` from `TimelineEventFocusKind::Event` to `AnyPaginator::Unthreaded`
2025-09-23 14:13:34 +02:00
Jorge Martín a0eecac8e0 chore(doc): Fix ThreadedEventsLoader::paginate_forwards docs 2025-09-23 14:13:34 +02:00
Jorge Martín 27ba6d070b feat(ffi): Expose TimelineEvent::thread_root_event_id 2025-09-23 14:13:34 +02:00
Jorge Martín 14ca34b09b feat(ffi): Add Room::load_or_fetch_event to the FFI layer
This way we can retrieve random events in a room and check their properties - this is needed to decide whether a permalink for an event should open in a thread or not
2025-09-23 14:13:34 +02:00
Jorge Martín 2166de7b0d refactor(sdk+ui): Make TimelineFocus::Event { paginator } generic
This way we can have the same focus handling both the focused event pagination in the main timeline with the `Paginator` and the focused event pagination in a thread with `EventThreadsLoader`.

The actual paginator is populated in `TimelineController::init_focus` after we call `/context` and can check if the event is part of a thread.
2025-09-23 14:13:34 +02:00
Jorge Martín fd66ae9226 feat(sdk): Add forwards pagination to ThreadEventsLoader
Make the existing `token` field a new `tokens` one with `PaginationTokens` type.

# Conflicts:
#	crates/matrix-sdk/src/paginators/thread.rs
2025-09-23 14:13:34 +02:00
Shrey Patel 56100dfa00 chore(search): Update README. 2025-09-23 11:27:55 +02:00
Benjamin Bouvier 2b567e18bc refactor(timeline): don't require an ExactSizeIterator on replace_with_initial_remote_events
This is only used to know if the new events list is empty or not, which
we can figure thanks to a peekable iterator.
2025-09-22 15:31:02 +02:00
Benjamin Bouvier a5e84230c7 chore(ffi): rejigger recent emoji code around so as to work with default features disabled
For some reason, running `cargo xtask ci clippy` locally would now fail,
complaining that the recent emoji functions didn't exist, in the FFI
layer. I suspect it's because some of the uniffi derive macro to export
functions incorrectly propagates the `cfg` guards; so a solution is to
move all this code under a new mod, that's enabled if and only if the
feature's enabled.
2025-09-22 11:42:10 +02:00
Benjamin Bouvier bf4a46e8de chore: rename a few badly named variables in the sql event cache store 2025-09-22 11:20:49 +02:00
Ivan Enderlin 659ae57218 fix(ui): room_list::sorters::recency is no longer based on 2 data.
This patch fixes an issue where the `recency` sorter is based on either
the latest event's timestamp, or the room recency stamp. This cannot
work with a sort algorithm as the position of a particular room can
be different based on what it is compared to (i.e. if the rooms have a
latest event value or not).

This patch updates the `recency` sorter to only use the recency stamp
for now, as the latest event is not yet computed for all rooms.
2025-09-19 17:39:05 +02:00
Shrey Patel dff6cb4414 refactor(search): Move RoomIndexBuilder into a submodule of index. 2025-09-19 16:37:09 +02:00
Shrey Patel 76348977d4 feat(search): Add encrypted search index support. 2025-09-19 16:37:09 +02:00
Shrey Patel a66e6822ed refactor(search): Add RoomIndexBuilder to create RoomIndex. 2025-09-19 16:37:09 +02:00
Shrey Patel b494303c07 feat(search): Implement an encrypted wrapper for a tantivy::directory::MmapDirectory. 2025-09-19 16:37:09 +02:00
Ivan Enderlin 0f0e37b677 chore: Update eyeball-im, eyeball-im-util and imbl.
This patch updates `eyeball-im` to 0.8.0, `eyeball-im-util` to 0.10.0
and `imbl` to 6.1.0.

The idea is to fix this bug https://github.com/jplatte/eyeball/pull/80.
2025-09-19 16:17:31 +02:00
Ivan Enderlin fc12a7340f chore(ui): Add a temporary entries_with_dynamic_adapters_with.
This patch adds a temporary
`RoomList::entries_with_dynamic_adapters_with` method to help debug an
issue in Element X.
2025-09-19 15:55:57 +02:00
Shrey Patel 80390346b1 feat(multiverse): Add search indexing at startup. 2025-09-19 14:25:02 +02:00
Shrey Patel 4b87dfea0b feat(sdk): Lazily create RoomIndex on search. 2025-09-19 14:25:02 +02:00
Shrey Patel 79aa0ab60d feat(search): Add bulk processing. 2025-09-19 14:25:02 +02:00
Shrey Patel a8ef44306a refactor(sdk): Move search index related code into its own module. 2025-09-19 14:25:02 +02:00
Shrey Patel b929f3e569 fix(search): Remove unused IndexError variants. 2025-09-19 14:25:02 +02:00
Ivan Enderlin 1c737e6569 bench: Run the room_list benchmark in the CI. 2025-09-19 11:06:45 +02:00
Benjamin Bouvier 8ae88e1e45 refactor(sdk): make the update_in_memory_caches method infallible 2025-09-18 18:00:27 +02:00
Benjamin Bouvier 768f9bfdb6 doc: fix a typo in a doc comment of invite_acceptance_details 2025-09-18 18:00:27 +02:00
Benjamin Bouvier 864d6c1a43 perf: avoid recomputing room notification modes on every sync
Some background knowledge: the room notification modes are functions of
the push rules events, and they will only change when the push rules
event changes. As a result, there are only two cases where we need to
recompute them:

- when the push rules event changed, we need to recompute all the room
notification modes, in case one has changed;
- when we run into a new room, we need to compute an initial value for
its room notification modes.

Based on these observations, this improves the code to avoid recomputing
the room notification mode on every single sync response. Instead,
they're computed if and only if the push rules event has changed, or for
new rooms only.

Also, this avoids reconstructing one `NotificationSettings` object per
room, since this would load from the database each time. Instead, a
single object is created (at most), and its `Rules` object is directly
accessed, to avoid repeatedly taking the lock on its internal `rules`
field.

This makes it so that the time spent under this method from tens of
milliseconds to less than 1 millisecond, in testing. See the pull
request initial post for more numbers.
2025-09-18 18:00:27 +02:00
Benjamin Bouvier da70aea5b0 feat(multiverse): allow not sharing pos at start
This is helpful to reproduce an initial sync response, and observe how
long it takes to process.
2025-09-18 18:00:27 +02:00
Benjamin Bouvier 1a9c7d5e2f test: add regression test that even if a room isn't in a sync response, its notification mode may be updated 2025-09-18 18:00:27 +02:00
Doug 9cd7760858 ffi: Expose is_direct on SpaceRoom. 2025-09-18 18:56:03 +03:00
Benjamin Bouvier 8575ed3f64 chore: define FrozenSlidingSyncPos only if the e2e-encryption feature is enabled
This caused compilation errors in other PRs, since the introduction of
Rust 1.90, which was able to detect this was unused otherwise.
2025-09-18 17:27:25 +02:00
Ivan Enderlin 1834f36136 doc(ui): Update the CHANGELOG.md file. 2025-09-18 15:38:16 +02:00
Ivan Enderlin 0bbefa000b chore(ui): Rename room_list_service::Room to RoomListItem.
This patch renames the `Room` type in `room_list_service` to
`RoomListItem` to avoid confusion with `matrix_sdk::Room`.
2025-09-18 15:38:16 +02:00
Ivan Enderlin a84c97b292 feat(ui): Introduce room_list_service::Room to cache data.
This patch improves throughput by +710% in `room_list_service` sorters
and filters. It introduces a new `room_list_service::Room` type that
derefs to `matrix_sdk::Room`. However, it **caches** some data from
`matrix_sdk::Room`. Why doing so? Because filters, but more specifically
sorters!, are calling methods on `matrix_sdk::Room`, so likely on
`matrix_sdk::RoomInfo`, quite intensively. `RoomInfo` is behind a
`SharedObservable`, which means it's behind a lock. Each time a sorter
sorts 2 rooms, the lock on `RoomInfo` can be called twice or more.

By caching the data, `RoomInfo` is reached once per refresh data, but
not during the filtering nor the sorting. It greatly reduces contention
on the `RoomInfo` lock, which improves the throughput by +710%, and the
time by -87%.

The cached data are refreshed in `merge_stream_and_receiver` when
(i) the stream of `Room` is updated, or when (ii) the stream of
`RoomInfoNotableUpdate` is updated. It's a central place it happens,
which isolates the behaviour.
2025-09-18 15:38:16 +02:00
Ivan Enderlin 94267d9597 chore(base): Rename Room::inner to Room::info.
This patch renames the `Room::inner` containing the `RoomInfo` to
`Room::info`.
2025-09-18 15:38:16 +02:00
Ivan Enderlin 62eb1996d9 feat(base): Add new_latest_event_timestamp and new_latest_event_is_local.
This patch adds 2 methods on `RoomInfo`: `new_latest_event_timestamp`
and `new_latest_event_is_local`, which respectively returns
`LatestEventValue::timestamp` and `LatestEventValue::is_local`. The goal
is to avoid cloning a `LatestEventValue` when it's useless. For example,
in the room list sorters!

This patch also updates the room list sorters `recency` and
`latest_event` to use these new methods. It improves the speed and
throughput by 18%.
2025-09-18 15:38:16 +02:00
Ivan Enderlin ec5c31a19d chore(bench): Add the room_list benchmark.
This patch adds the `room_list` benchmark. The goal is to measure
the time it takes to create a room list, to sort it, to filter and to
“display” it.
2025-09-18 15:38:16 +02:00
Jorge Martín 7e474c3a52 refactor(sdk): Remove UpdateGlobalAccountDataEndpoint and GlobalAccountDataEndpoint
These are meant to be replaced with specific endpoints for each case.

Added the `global_account_data_mock_builder` helper function to make building these mock endpoints a bit easier.
2025-09-18 13:12:49 +02:00
Jorge Martín 8706ad74b3 refactor(sdk): Immediately truncate the recent emoji list
This way we'll always work with a list of at most `MAX_RECENT_EMOJI_COUNT` items, which should ensure performance is good enough.
2025-09-18 13:12:49 +02:00
Jorge Martín 3cc88e5008 test(sdk): Redo matching the request body for the update recent emoji endpoint 2025-09-18 13:12:49 +02:00
Jorge Martín a937780623 fix(sdk): Use a max recent emoji count of 100
This prevents the account data from growing indefinitely and makes sure values that aren't recently used can be forgotten.
2025-09-18 13:12:49 +02:00
Benjamin Bouvier 9dc27698dd chore: address new clippy recommendations 2025-09-18 12:01:48 +02:00
Benjamin Bouvier 49d72cd992 chore: run rustfmt after moving to the next edition of matrix-sdk 2025-09-18 12:01:48 +02:00
Benjamin Bouvier 98e799da80 chore: remove unnecessary bindings modifiers
match ergonomics ftw! in the 2024 edition, these are necessary only when
the capturing mode is `move`.
2025-09-18 12:01:48 +02:00
Benjamin Bouvier ea386c9e64 chore: specify explicitly that some stream/futures don't capture any lifetime 2025-09-18 12:01:48 +02:00
Benjamin Bouvier bba2af9882 refactor: don't use the reserved keyword gen in tests
I *think* these compare a generated URL against an expected one, but the
naming is a bit strange in those tests. It's just tests, after all.
2025-09-18 12:01:48 +02:00
Benjamin Bouvier 51934dd249 test: stop using the unit never type fallback
By making it explicit that the async closures return a unit type, we can
get rid of these two allow lines, and prepare for migrating this code to
the 2024 edition.
2025-09-18 12:01:48 +02:00
Benjamin Bouvier 07924ad4e4 chore: bump matrix-sdk to edition 2024 2025-09-18 12:01:48 +02:00
Michael Goldenberg 1312a27597 feat(indexeddb): add IndexedDB-backed impls for getting, adding, replacing, and removing media
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 17:03:39 +02:00
Michael Goldenberg 038207870a refactor(indexeddb): add media-related functions to media store transaction type
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 17:03:39 +02:00
Michael Goldenberg eb62ac9fad refactor(indexeddb): use UnixTime type to represent Media::last_access
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 17:03:39 +02:00
Michael Goldenberg 79154bd03d refactor(indexeddb): add type for representing time relative to unix epoch
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 17:03:39 +02:00
Michael Goldenberg c7990e6e33 refactor(indexeddb): replace media source index with media uri index
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 17:03:39 +02:00
Michael Goldenberg c839c01205 refactor(indexeddb): add fns for putting an item into IndexedDB if the serialized value satisfies predicate
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 17:03:39 +02:00
Michael Goldenberg 62d2d0ff94 fix(indexeddb): fix import in state store migration tests
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg bcae429062 refactor(indexeddb): tweak features and imports to ensure types and traits are only available when they are needed
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg b0e9f3c666 refactor(indexeddb): expose safe encode trait even when e2e-encryption feature is not enabled
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg a325105190 feat(indexeddb): add experimental encrypted state events feature to quiet warning about using non-existent features in crypto store
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 3835a7ff94 refactor(indexeddb): remove unused imports from transaction module
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg ead9400702 refactor(indexeddb): allow dead code in transaction and indexed type serializer modules while media store under development
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 78172bb7b6 refactor(indexeddb): remove unused imports and dead code from event cache store module
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg bbf2164ab2 refactor(indexeddb): allow dead code in event cache store builder until it is publicly exposed
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 46a2ee6177 fix(indexeddb): handle result in event cache store migrations
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg f1caf8f27f refactor(indexeddb): remove unused imports in media store module
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 60bfc48b6b refactor(indexeddb): allow dead code in media store module as it is still under development
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg afa339c02b refactor(indexeddb): remove extraneous core object store from event cache store migrations
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg c9b7fc7007 refactor(indexeddb): rename serializer types modules to indexed_types
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 2f6bb3a1eb refactor(indexeddb): move module-specific constants into their own modules
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg a259860221 refactor(indexeddb): deduplicate constants for types from std
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 2765c18e61 refactor(indexeddb): move custom bool serializer into serializer module
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg b44b6478c0 refactor(indexeddb): nest generalized transaction in event cache store transaction
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 0bebf144d1 refactor(indexeddb): nest generalized transaction in media store transaction
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 975b08c019 refactor(indexeddb): add generalized transaction type and error
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 453613c13f refactor(indexeddb): deduplicate async error deps trait
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg cb94969e2a refactor(indexeddb): deduplicate serializer traits and types
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 0e4e4eae2b refactor(indexeddb): rename IndexeddbMediaStoreSerializer{Error} to IndexedTypeSerializer{Error}
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg b9410dff61 refactor(indexeddb): rename IndexeddbEventCacheStoreSerializer{Error} to IndexedTypeSerializer{Error}
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 013bb9a5ac refactor(indexeddb): rename IndexeddbSerializer to SafeEncodeSerializer
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Michael Goldenberg 891ed0efff refactor(indexeddb): move SafeEncode-related traits and types into their own module
Signed-off-by: Michael Goldenberg <m@mgoldenberg.net>
2025-09-17 14:00:43 +02:00
Ivan Enderlin 2e3be13b4d fix(ui): The recency room list sorter stops using the latest event's timestamp.
This patch updates the `recency` room list sorter to no longer
use the `LatestEventValue::timestamp` method. It keeps using the
`Room::recency_stamp` for the moment, as it was the case before. This
patch is a test to try finding the problem in some Matrix clients where
the room list becomes unusable. We suspect it's because of this patch
sorter.
2025-09-17 13:32:42 +02:00
Ivan Enderlin efcb7125ad chore(ffi): Define new log target for deserialized_responses.
This patch defines a new log target,
`MatrixSdkCommonDeserializedResponses`. It is enabled by the
`SyncProfiling`, `EventCache` or `Timeline` log packs.

This patch also changes the level of the log in
`TimelineEvent::timestamp` from `trace` to `warn`.
2025-09-17 11:33:14 +02:00
Valere Fedronic 681863423c feat(rtc): Remove deprecated CallNotify in favour of RtcNotification
`CallNotify` event has been deprecated in favour of `RtcNotification` event https://github.com/ruma/ruma/pull/2199
2025-09-16 16:06:39 +02:00
Stefan Ceriu 8c6922d5a9 feat(spaces): use the space children_state received from /hierarchy to populate children via parameters and expose them on SpaceRooms 2025-09-16 12:55:58 +03:00
Benjamin Bouvier 8c60ef2635 refactor(timeline): more refactorings around timeline focus
This includes a new code location where we'd need to handle
permalink-in-thread differently, and reduces the number of matches on
the focus kind.
2025-09-16 11:51:10 +02:00
Benjamin Bouvier 3e9e74a888 feat(timeline): add a public is_threaded() method to know if a timeline is focused on a thread or not
And use fewer `matches!` statements to figure whether a timeline is
threaded or not, or what its thread root is.
2025-09-16 11:51:10 +02:00
Benjamin Bouvier a06403c12f refactor(timeline): use a getter to figure if a focus is on a thread
This will pave the way for permalink targets which are for events in a
thread, by making it possible to add a future condition in the
`TimelineFocusKind::Event` case (if the pagination used under the hood
is using /relations, then it's a thread).
2025-09-16 11:51:10 +02:00
Ivan Enderlin d3a7d26c7d chore(common): Add log in TimelineEvent::timestamp.
This patch adds a log in `TimelineEvent::timestamp` when the `timestamp`
has to be extracted. It can be a performance problem depending on when
it's called.
2025-09-16 10:54:05 +02:00
Benjamin Bouvier e83f37e68b test: add test for the previous commit 2025-09-15 18:02:34 +02:00
Benjamin Bouvier efda12058f fix(room service): enable the thread subscriptions extension iff the server advertises support for it 2025-09-15 18:02:34 +02:00
dependabot[bot] f12ee861b0 chore(deps): bump tj-actions/changed-files from 46.0.5 to 47.0.0
Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 46.0.5 to 47.0.0.
- [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/v46.0.5...v47.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 17:22:23 +02:00
dependabot[bot] 48cc68c466 chore(deps): bump CodSpeedHQ/action from 4.0.0 to 4.0.1
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 4.0.0 to 4.0.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/6eeb021fd0f305388292348b775d96d95253adf4...653fdc30e6c40ffd9739e40c8a0576f4f4523ca1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 17:15:01 +02:00
Skye Elliot acaff39594 fix(crypto): Report inner-outer state key differences as invalid.
Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2025-09-15 15:13:25 +02:00
Stefan Ceriu bdc564bb55 chore(spaces): rewrite /hierarchy room list filtering and parent space extraction to avoid mixing responsibilities. 2025-09-15 15:31:49 +03:00
Stefan Ceriu ac68c4a47d chore(ffi): expose the new SpaceRoomList space and its respective updates publisher. 2025-09-15 15:31:49 +03:00
Stefan Ceriu 75a5c19f91 chore(spaces): have SpaceRoom::new_from_known work with a reference and reduce the number of clones required. 2025-09-15 15:31:49 +03:00
Stefan Ceriu 7a53615d80 feat(spaces): expose an is_direct flag on SpaceRooms 2025-09-15 15:31:49 +03:00
Stefan Ceriu 802e137ae5 chore(spaces): align on calling the owner of the room list a space and not a parent_space 2025-09-15 15:31:49 +03:00
Stefan Ceriu 68cb3fb6a4 feat(spaces): expose the parent space on the SpaceRoomList 2025-09-15 15:31:49 +03:00
Jorge Martín cc1fbf9882 refactor(ffi): Make EC optional parameters default to None
This is done so we can just use:

```kotlin
VirtualElementCallWidgetConfig(intent = ...)
```

Instead of:

```kotlin
VirtualElementCallWidgetConfig(
    intent = ...,
    skipLobby = null,
    header = null,
    hideHeader = null,
    preload = null,
    ...
)
```
2025-09-15 11:21:49 +02:00
Richard van der Hoff 89c1c8e4fa changelog 2025-09-12 17:13:08 +01:00
Richard van der Hoff 423f15a125 test(crypto): add a test for verification_request_content 2025-09-12 17:13:08 +01:00
Richard van der Hoff 4f2cd1c5ec crypto(test): utility function for creating VerificationMachines 2025-09-12 17:13:08 +01:00
Andy Balaam 8a6c4fdcb4 Return a MessageType from verification_request_content 2025-09-12 17:13:08 +01:00
Richard van der Hoff b8cbd6c448 Merge pull request #5654 from matrix-org/rav/identity_test_cleanups
crypto: Simplify `PrivateCrossSigningIdentity::with_account`
2025-09-12 14:21:19 +01:00
Ivan Enderlin 5ccbc1c378 fix(sqlite): Empty the cache after the introduction of TimelineEvent::timestamp.
After the merge of
https://github.com/matrix-org/matrix-rust-sdk/pull/5648, we want
all events to get a `TimelineEvent::timestamp` value (extracted from
`origin_server_ts`).

To accomplish that, we are emptying the event cache. New synced events
will be built correctly, with a valid `TimelineEvent::timestamp`,
allowing a clear, stable situation.
2025-09-12 15:06:46 +02:00
Richard van der Hoff 0002ea46ab crypto: inline PrivateCrossSigningIdentity::with_account
This was now only used in one place, and I think it makes more sense to inline
it into olm::Account than leave it in `PrivateCrossSigningIdentity`.
2025-09-12 14:04:02 +01:00
Richard van der Hoff bb46dc74d0 crypto: Factor out PrivateCrossSigningIdentity::for_account
It turns out that creating a cross-signing identity *without* the upload
requests is a very common thing to do, especially in tests. We can simplify
some code by factoring it out as a new helper.
2025-09-12 14:04:02 +01:00
Shrey Patel c2bc465c06 feat(base): Add get_room_events to EventCacheStore trait and impls. 2025-09-12 12:59:22 +02:00
Ivan Enderlin b788ba0d73 feat(base) LatestEventValue::timestamp uses the new TimelineEvent::timestamp method.
This patch updates `LatestEventValue::timestamp` to use
the new `TimelineEvent::timestamp` method in case of a
`LatestEventValue::Remote`.
2025-09-12 11:43:49 +02:00
Ivan Enderlin 53a74f3949 feat(common): Add TimelineEvent::timestamp.
This patch adds the `timestamp` field to `TimelineEvent`.
It's a copy of the `origin_server_ts` value, parsed as an
`Option<MilliSecondsSinceUnixEpoch>`. It's `None` if the parsing failed,
or if the `TimelineEvent` was deserialised from a version before this
new field was added.

A new `extract_timestamp` function is added for this purpose. It
protects against malicious `origin_server_ts` where the value can be
set to year 2100 for example. The only protection we are adding here is
to take the `min(origin_server_ts, now())`, so that the event can never
been “in the future”.

It doesn't protect against a malicious value like 0. It's non-trivial to
define a minimum timestamp for an event.

When a `TimelineEvent` is mapped from one kind to another kind, the
`timestamp` is carried over. To achieve that, new `to_decrypted` and
`to_utd` methods are added.

The rest of the code is updated accordingly.
2025-09-12 11:43:49 +02:00
Damir Jelić 215ca3d798 Merge pull request #5641 from matrix-org/feat/element-recent-emojis
feat: Add Element recent emojis for shared emoji reactions
2025-09-12 11:32:12 +02:00
Jorge Martín 87032a36bd fix: use the same ordering as in Element Web: first, sort by count descending, then by recency for items with equal count 2025-09-12 08:21:14 +02:00
Jorge Martín 4f881b55f9 refactor: link to the Element Web implementation 2025-09-12 08:21:14 +02:00
Jorge Martín bb9bdee4a7 refactor: fix doc comment issues 2025-09-12 08:21:14 +02:00
Jorge Martín 4216ec6113 test: Add serialization and deserialization tests for recent emojis 2025-09-12 08:21:14 +02:00
Jorge Martín b2df2742bd refactor: Move the recent emoji functions from client to account.
Also, make sure updating the emojis first fetches the most up-to-date data.
2025-09-12 08:21:14 +02:00
Jorge Martín 163ed929fe refactor: Add docs to test helper 2025-09-12 08:21:14 +02:00
Jorge Martín 9776ae6acd refactor: rename feature to experimental-element-recent-emoji 2025-09-12 08:21:14 +02:00
Jorge Martín 68c2b89bf5 fix: Fix clippy 2025-09-12 08:21:14 +02:00
Jorge Martín b744e5789a fix: Fix test in doc comment 2025-09-12 08:21:14 +02:00
Jorge Martín 231840f6ae refactor: Make the ci task also use the element-recent-emojis feature for different tasks 2025-09-12 08:21:14 +02:00
Jorge Martín fc224b17c7 refactor(ui): Make toggle_reaction return a bool so we know if the emoji was added or removed 2025-09-12 08:21:14 +02:00
Jorge Martín 5522509e6b feat(ffi): Add bindings for the recent emojis 2025-09-12 08:21:14 +02:00
Jorge Martín 89fd0b5e53 feat(sdk): Allow adding and retrieving recent emojis
Include some tests.
2025-09-12 08:21:14 +02:00
Jorge Martín eee1fa2b71 feat(sdk-base): Add Element recent emojis event
Add a feature for it too
2025-09-12 08:21:14 +02:00
Timo K 62763ca000 fix element call url "intent" serialization
Signed-off-by: Timo K <toger5@hotmail.de>
2025-09-12 07:52:37 +03:00
Johannes Marbach 5e573417cb fix(timeline): avoid replacing timeline items when the encryption info is unchanged
Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2025-09-11 15:48:39 +01:00
Damir Jelić c3621f2bd1 Merge branch 'release-0.14' 2025-09-11 13:34:56 +02:00
Damir Jelić 0eac2a099f chore(base): Add the CVE ID for the power level panic to the changelog 2025-09-11 13:33:33 +02:00
Kévin Commaille 90eb403c18 sqlite: Drop media table from event cache store
Since the media store was split into a separate database.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
2025-09-11 11:43:04 +01:00
Ivan Enderlin 878e02b652 doc(common): Rephrase the documentation of cross_process_lock a little bit. 2025-09-10 20:35:45 +02:00
Ivan Enderlin bbe8f17b1a refactor(common): Rename LockStoreError to CrossProcessLockError.
This patch renames the `LockStoreError` enum to `CrossProcessLockError`
to be consistent with the other types in the same module.

The `BackingStoreError` variant is also renamed to `TryLockError`.
2025-09-10 20:35:45 +02:00
Ivan Enderlin f65bb6016c refactor(common): Rename store_locks to cross_process_lock. 2025-09-10 20:35:45 +02:00
Ivan Enderlin 976eacb624 refactor(common): Rename BackingStore to TryLock.
This patch renames the `BackingStore` trait to `TryLock`. It also
renames the `CrossProcessLock::store` field to `locker`. It's not
necessarily a store, it can be anything.
2025-09-10 20:35:45 +02:00
Ivan Enderlin 5fe5cfd85f refactor(common): Rename CrossProcessStoreLock* to CrossProcessLock*.
This patch renames `CrossProcessStoreLock` and
`CrossProcessStoreLockGuard` to `CrossProcessLock` and
`CrossProcessLockGuard`.
2025-09-10 20:35:45 +02:00
Ivan Enderlin 0233ac906e chore(common): Simplify code.
This patch simplifies a code that does a surprising thing. The `#[cfg]`
isn't necessary here.
2025-09-10 20:35:45 +02:00
Damir Jelić 4cc1cd1913 chore(sdk): Better logs for the duplicate one-time key error 2025-09-10 13:09:35 +00:00
Valere 2248bbf6ab fix adding both final and dev id doesn't work with ruma alias 2025-09-10 13:07:48 +02:00
dragonfly1033 2afbdfae0b Split media store from event cache store. (#5568)
This PR is a start to the process of splitting the media store from the
event cache store. #5410

It contains:
* Split `MediaStore` trait from `EventCacheStore`. 
* Rename `EventCacheStoreMedia` to `MediaStoreInner`. 
* Move relevant tests into `MediaStoreIntegrationTests`.

This will be done over 3 PR's (reviewing 1, 2, 3 then merging 3 into 2
into 1).

A reminder comment for my own sanity:
This PR will not pass tests until after merging.

Current state of this PR:
- [x] Step 1 reviewed #5568
- [x] Step 2 reviewed #5569 
- [x] Step 3 reviewed #5571 
- [x] Step 3 merged into Step 2
- [x] Step 2 merged into Step 1
- [ ] Add changes to changelog.
- [ ] Ready to merge 🎉 

Note, may also want to: 
* Re-organize file structure
* Split/refactor benchmarks namely `benchmarks/benches/event_cache.rs`

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

- [ ] 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: Shrey Patel shreyp@element.io

---------

Co-authored-by: Shrey Patel <shreyp@element.io>
2025-09-10 12:03:02 +02:00
Timo d2ca0262ae feat(element-call url params): split url params into configuration and properties (#5560)
This PR is part of an onging effort to move responsiblity to the EC app
and out of the EX apps.

4 intends (f.ex `join_existing` `start_new_dm`... ) (as url paramters)
are introduced in recent element call versions. Those intends behave
like defaults. If an intend is set a set of url parameters are
predefined.
Not all params can be covered by the intend (for insteance the
`widget_id` or the `host_url`).
This PR splits the url parameters into configuration (things that can be
configured by the intent) and properties (things that still need to be
passed one by one)


The goal with this change is that EX only needs to configre the intent
once and the EC codebase can update the behavior in those 4 specific
scenarios in case new features come along (auto hangup when other
participants leave, send call ring notification...)


Signed-off-by: Timo K <toger5@hotmail.de>

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

- [ ] 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: Timo K <toger5@hotmail.de>
2025-09-10 11:46:21 +02:00
Damir Jelić f1064425bd Merge branch 'release-0.14' into poljar/merge-0.14-back-to-main 2025-09-10 11:20:39 +02:00
Damir Jelić 5ef3ecac8c chore: Allow the adler crate despite it being unmaintained 2025-09-10 10:41:18 +02:00
Damir Jelić 6c537d74de chore: Release matrix-sdk-base version 0.14.1 2025-09-10 10:41:18 +02:00
Damir Jelić 476fe5f9d2 fix(base): Fix a panic when we encounter a power level at Int::Min 2025-09-10 10:41:18 +02:00
Damir Jelić 186132c248 test(base): Test how we normalize power levels at the int limits 2025-09-10 10:41:18 +02:00
Damir Jelić 0f90631d4a test(base): Add a proptest to validate our normalize_power_level method 2025-09-10 10:41:18 +02:00
Damir Jelić 80262f2f36 chore: Put the power level normalization logic into a separate function 2025-09-10 10:41:18 +02:00
Damir Jelić 91c5f8a01a chore: Add some missing PR links in our changelog 2025-09-10 10:41:18 +02:00
Valere 502d6d3095 widget capabilities rtc decline test 2025-09-09 18:22:49 +02:00
Valere db4ce0bea5 widget-driver: Add read/send capabilities for rtc decline event 2025-09-09 18:22:49 +02:00
Shrey Patel b0c0e0e0c4 feat(search): Add paginated search. 2025-09-09 16:24:25 +02:00
Ivan Enderlin 5c78ddec13 refactor(ui): Remove intermediate structs.
This patch removes intermediate structs and uses a function directly.
2025-09-09 15:48:39 +02:00
Ivan Enderlin 59a62550e6 refactor(ui): Remove intermediate structs.
This patch removes intermediate structs and uses a function directly.
2025-09-09 15:48:39 +02:00
Ivan Enderlin fd356a9e17 chore(ui): Introduce the notion of _rank_ in the recency sorter.
This patch adds the notion of _rank_ in the `recency` sorter to avoid
confusion around `u64`: is it a timestamp or a recency stamp? It's
purely semantics, but I hope it clarify the code.
2025-09-09 15:48:39 +02:00
Ivan Enderlin fc69b2683f refactor(ui): Rename *Matcher to *Sorter in room_list_service::sorters.
This patch renames all the structure `*Matcher` to `*Sorter`.
And their `matches` method become `cmp`. It was copy-pasted from
`room_list_service::filters` probably, but the semantics here are not
_matcher_ but _sorter_. It's more consistent that `cmp` returns an
`Ordering`.
2025-09-09 15:48:39 +02:00
Ivan Enderlin 5c94177581 doc(base): Fix documentation of recency_stamp. 2025-09-09 15:48:39 +02:00
Ivan Enderlin 0335785e67 feat(base): Introduce the RoomRecencyStamp type.
This patch adds the `RoomRecencyStamp` type to avoid confusion with other
`u64` values.
2025-09-09 15:48:39 +02:00
Ivan Enderlin c860be4969 feat(ui): Use the new latest_event sorter in the room list.
This patch installs the `new_sorter_latest_event` in the room list.
2025-09-09 15:48:39 +02:00
Ivan Enderlin 8ff7e58bc0 doc(ui): Fix a typo. 2025-09-09 15:48:39 +02:00
Ivan Enderlin 01c0775e59 feat(ui): Update the recency sorter to include the LatestEventValue.
This patch updates the `recency` sorter of the room list to rely on the
`LatestEventValue`'s timestamp, or on the `bump_stamp` returned by the
sync. Using the `LatestEventValue`'s timestamp is more reliable as we
don't rely on the server. However, we must be careful to compare values
of the same nature because the timetamp from the `LatestEventValue` and
the `bump_stamp` doesn't represent the same thing! The `bump_stamp` is
only used when the value for the `LatestEventValue` is `None`.

It's a compromise to get a more accurate listing. Though,
`LatestEventValue::timestamp` returns the `origin_server_ts` value,
which can be forged by a malicious user (then a room could be _sticked_
at the top or at the bottom of the room list). Note that this problem
already existed in the past before the server computed a `bump_stamp`.
Also note that some homeservers use the `origin_server_ts` as the
`bump_stamp` value. Anyway, it's not a security risk as far as I know.
2025-09-09 15:48:39 +02:00
Ivan Enderlin bd3ddc19e9 feat(base): Implement LatestEventValue::timestamp.
This patch implements a `LatestEventValue::timestamp` method to fetch
the timestamp of a latest event value.
2025-09-09 15:48:39 +02:00
Ivan Enderlin 8156bc25f8 feat(ui): Add the new latest_event sorter for the room list.
This patch implements the new `latest_event` sorter for the room list
which puts the local latest events before the other kinds (like `Remote`
or `None`).
2025-09-09 15:48:39 +02:00
Damir Jelić 441b006c5f ci: Bump the codspeed action and define our benchmark mode 2025-09-09 14:45:49 +02:00
Damir Jelić ce3b67f801 Update bindings/matrix-sdk-ffi/CHANGELOG.md
Co-authored-by: Ivan Enderlin <ivan@mnt.io>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-09-09 10:06:21 +02:00
Damir Jelić 260037c4c7 Remove the normalized power level from the bindings
The field is reportedly unused so there's no need to spend time to
calculate the value and pass it over the FFI.
2025-09-09 10:06:21 +02:00
Damir Jelić a93274de36 fix(base): Fix a panic when we encounter a power level at Int::Min 2025-09-09 10:06:21 +02:00
Damir Jelić 77b426e1aa test(base): Test how we normalize power levels at the int limits 2025-09-09 10:06:21 +02:00
Damir Jelić 251530b6f4 test(base): Add a proptest to validate our normalize_power_level method 2025-09-09 10:06:21 +02:00
Damir Jelić 46c7338509 chore: Put the power level normalization logic into a separate function 2025-09-09 10:06:21 +02:00
Damir Jelić 9e2f2b3534 chore: Add some missing PR links in our changelog 2025-09-09 09:47:49 +02:00
dependabot[bot] 03c6dd9bfc chore(deps): bump crate-ci/typos from 1.35.7 to 1.36.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.35.7 to 1.36.2.
- [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.35.7...v1.36.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 16:52:16 +02:00
dependabot[bot] 3b7a626b8f chore(deps): bump actions/github-script from 7 to 8
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 16:51:53 +02:00
dependabot[bot] 2e7bea9253 chore(deps): bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 16:50:02 +02:00
dependabot[bot] c7de40b54d chore(deps): bump CodSpeedHQ/action from 3.8.1 to 4.0.0
Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 3.8.1 to 4.0.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/76578c2a7ddd928664caa737f0e962e3085d4e7c...6eeb021fd0f305388292348b775d96d95253adf4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 15:49:20 +01:00
Shrey Patel 086233ad5f feat(multiverse): Show messages in search results. 2025-09-08 14:05:28 +02:00
Valere 412b7bbc7b add changelog 2025-09-06 11:15:49 +02:00
Valere 0617c88c1c fix example compilation 2025-09-06 11:15:49 +02:00
Valere 83b390204d Add an example for subscribe_to_call_decline_events 2025-09-06 11:15:49 +02:00
Valere 4ef249dc6e review: quick new lines for clarity 2025-09-06 11:15:49 +02:00
Valere 0aef2559bd review: Remove unneeded msc4310 feature (consistent with msc4075) 2025-09-06 11:15:49 +02:00
Valere 653a00351c review: Remove unneeded generic and remove early returns 2025-09-06 11:15:49 +02:00
Valere 8606ac3dfb fix typo 2025-09-06 11:15:49 +02:00
Valere db5503e30e tests: add decline call tests 2025-09-06 11:15:49 +02:00
Valere c51b4f03a2 misc: clippy fixes 2025-09-06 11:15:49 +02:00
Valere 0fc0a5514d guard test behing feature flag 2025-09-06 11:15:49 +02:00
Valere e83af1aae2 tests: subscribe_to_call_decline_events 2025-09-06 11:15:49 +02:00
Valere ab58c376dd bindings: MSC4310 call decline and subscribe to decline events 2025-09-06 11:15:49 +02:00
Damir Jelić 6c8fb507a2 chore: Allow the adler crate despite it being unmaintained 2025-09-06 11:15:08 +02:00
Ivan Enderlin b1c28f4bc1 feat(ui): sync_service::State::Error contains the cause error.
This patch updates the `State::Error` variant to contain the error that
led to this state.
2025-09-05 22:31:53 +02:00
Ivan Enderlin 6dbdffd36e refactor(ui): sync_service::State no longer implements PartialEq.
This patch removes the `PartialEq` implementation on
`sync_service::State`. It was only used for test purposes. Outside that,
it doesn't make sense.
2025-09-05 22:31:53 +02:00
Ivan Enderlin e45387b65b refactor(ui): Encode more states in type systems.
This patch updates the signature of `TerminationReport`'s
constructors so that it's impossible to create invalid states, like
an `origin` of `TerminationOrigin::RoomList` with an error of type
`encryption_sync_service::Error`. The constructors force the error to
match the origin.
2025-09-05 22:31:53 +02:00
Ivan Enderlin b8803cb465 refactor(ui): Remove TerminationReport::has_expired.
This patch moves `SyncTaskSupervisor::check_if_expired` to
`TerminationReport::has_expired`. Because `TerminationReport` now holds
the error, we can remove the `has_expired` field and get a `has_expired`
method!
2025-09-05 22:31:53 +02:00
Ivan Enderlin 3c88b46c54 feat(ui): TerminationReport contains the error if any.
This patch changes the `TerminationReport::is_error` field to become
`error: Option<Error>`. This patch also creates new constructor on
`TerminationReport` to simplify the code.
2025-09-05 22:31:53 +02:00
Damir Jelić d25632507d Merge pull request #5628 from matrix-org/release-0.14
Merge back release branch for 0.14
2025-09-04 19:19:59 +02:00
623 changed files with 64363 additions and 27425 deletions
+4
View File
@@ -7,3 +7,7 @@ crates-io = "https://docs.rs/"
[unstable]
rustdoc-map = true
[target.aarch64-linux-android]
# These rust flags improve the performance on Android on arm64
rustflags = ["-C", "target-feature=+neon,+aes,+sha2,+sha3,+pmuv3"]
+3 -3
View File
@@ -10,6 +10,7 @@ exclude = [
version = 2
ignore = [
{ id = "RUSTSEC-2024-0436", reason = "Unmaintained paste crate, not critical." },
{ id = "RUSTSEC-2024-0388", reason = "Unmaintained derivative crate, not a direct dependency" },
]
[licenses]
@@ -27,9 +28,6 @@ allow = [
"Unicode-3.0",
"Zlib",
]
exceptions = [
{ allow = ["Unicode-DFS-2016"], crate = "unicode-ident" },
]
[bans]
# We should disallow this, but it's currently a PITA.
@@ -53,4 +51,6 @@ allow-git = [
# We can release vodozemac whenever we need but let's not block development
# 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"
]
+1
View File
@@ -1,2 +1,3 @@
* @matrix-org/rust
/crates/matrix-sdk-crypto @matrix-org/rust @matrix-org/rust-crypto-reviewers
/crates/matrix-sdk-indexeddb/src/crypto_store @matrix-org/rust @matrix-org/rust-crypto-reviewers
+62 -3
View File
@@ -19,9 +19,67 @@ jobs:
- linked_chunk
- store_bench
- timeline
- room_list
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
# This CI workflow can run into space issue, so we're cleaning up some
# space here.
- name: Create some more space
run: |
echo "Disk space before cleanup"
df -h
cd /opt
find . -maxdepth 1 -mindepth 1 '!' -path ./containerd '!' -path ./actionarchivecache '!' -path ./runner '!' -path ./runner-cache -exec rm -rf '{}' ';'
rm -rf /opt/hostedtoolcache
# Get rid of binaries and libs we're not interested in.
sudo rm -rf \
/usr/local/julia* \
/usr/local/aws*
sudo rm -rf \
/usr/local/bin/minikube \
/usr/local/bin/node \
/usr/local/bin/stack \
/usr/local/bin/bicep \
/usr/local/bin/pulumi* \
/usr/local/bin/helm \
/usr/local/bin/azcopy \
/usr/local/bin/packer \
/usr/local/bin/cmake-gui \
/usr/local/bin/cpack
sudo rm -rf \
/usr/local/share/powershell \
/usr/local/share/chromium
sudo rm -rf /usr/local/lib/android
echo "::group::/usr/local/bin/*"
du -hsc /usr/local/bin/* | sort -h
echo "::endgroup::"
echo "::group::/usr/local/share/*"
du -hsc /usr/local/share/* | sort -h
echo "::endgroup::"
echo "::group::/usr/local/*"
du -hsc /usr/local/* | sort -h
echo "::endgroup::"
echo "::group::/usr/local/lib/*"
du -hsc /usr/local/lib/* | sort -h
echo "::endgroup::"
echo "::group::/opt/*"
du -hsc /opt/* | sort -h
echo "::endgroup::"
echo "Disk space after cleanup"
df -h
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@ede6de059f8046a5e236c94046823e2af11ca670
@@ -31,10 +89,11 @@ jobs:
bins: cargo-codspeed
- name: Build the benchmark target(s)
run: cargo codspeed build -p benchmarks ${{ matrix.benchmark }} --features codspeed
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
- name: Run the benchmarks
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c
uses: CodSpeedHQ/action@4deb3275dd364fb96fb074c953133d29ec96f80f
with:
run: cargo codspeed run
mode: "instrumentation"
token: ${{ secrets.CODSPEED_TOKEN }}
+8 -8
View File
@@ -31,7 +31,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -52,7 +52,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -69,10 +69,10 @@ jobs:
steps:
- name: Checkout Rust SDK
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Checkout Kotlin Rust Components project
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: matrix-org/matrix-rust-components-kotlin
path: rust-components-kotlin
@@ -107,7 +107,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -136,7 +136,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
@@ -161,7 +161,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-macos }}"
@@ -191,7 +191,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
# install protoc in case we end up rebuilding opentelemetry-proto
- name: Install protoc
+24 -35
View File
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -68,7 +68,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -85,7 +85,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -99,7 +99,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -116,7 +116,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install libsqlite
run: |
@@ -125,6 +125,8 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Load cache
uses: Swatinem/rust-cache@v2
@@ -135,7 +137,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -167,7 +169,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -239,7 +241,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
@@ -248,7 +250,7 @@ jobs:
components: clippy
- name: Install wasm-pack
uses: qmaru/wasm-pack-action@v0.5.1
uses: qmaru/wasm-pack-action@v0.5.3
if: '!matrix.check_only'
with:
version: v0.13.1
@@ -268,7 +270,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -289,10 +291,10 @@ jobs:
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.35.7
uses: crate-ci/typos@v1.43.0
lint:
name: Lint
@@ -301,7 +303,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -311,7 +313,7 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-08-08
toolchain: nightly-2025-10-01
components: clippy, rustfmt
- name: Load cache
@@ -320,7 +322,7 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -360,7 +362,7 @@ jobs:
steps:
- name: Checkout the repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install libsqlite
run: |
@@ -384,24 +386,11 @@ jobs:
HOMESERVER_URL: "http://localhost:8008"
HOMESERVER_DOMAIN: "synapse"
run: |
cargo nextest run -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
cargo nextest run --profile ci -p matrix-sdk-integration-testing --features "${{ matrix.feature }}"
compile-bench:
name: 🚄 Compile benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v5
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Load cache
uses: Swatinem/rust-cache@v2
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Compile benchmarks (no run)
run: |
cargo bench --profile dev --no-run
files: ./target/nextest/ci/junit.xml
token: ${{ secrets.CODECOV_TOKEN }}
+3 -3
View File
@@ -97,7 +97,7 @@ jobs:
df -h
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
@@ -129,7 +129,7 @@ jobs:
uses: taiki-e/install-action@nextest
- name: Get xtask
uses: actions/cache/restore@v4
uses: actions/cache/restore@v5
with:
path: target/debug/xtask
key: "${{ needs.xtask.outputs.cachekey-linux }}"
@@ -167,7 +167,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@v4
uses: actions/upload-artifact@v6
with:
name: codecov_report
path: |
+1 -1
View File
@@ -10,5 +10,5 @@ jobs:
cargo-deny:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: EmbarkStudios/cargo-deny-action@v2
+2 -2
View File
@@ -17,10 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v46.0.5
uses: tj-actions/changed-files@v47.0.1
- name: Detect long path
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} # ignore the deleted files
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Machete
uses: bnjbvr/cargo-machete@v0.9.1
uses: bnjbvr/cargo-machete@78beac95c8fd7c25bdfb194415128523e41512d5
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install protoc
uses: taiki-e/install-action@v2
@@ -31,10 +31,10 @@ jobs:
- name: Install Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-08-08
toolchain: nightly-2025-10-01
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
+1 -1
View File
@@ -7,6 +7,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Block Fixup Commit Merge
uses: 13rac1/block-fixup-merge-action@v2.0.0
+1 -1
View File
@@ -11,6 +11,6 @@ jobs:
msrv:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --rust-version --workspace --all-targets --ignore-private
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- name: 'Fetch coverage report from artifacts'
id: prepare_report
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
var fs = require('fs');
@@ -58,7 +58,7 @@ jobs:
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
path: repo_root
+2 -2
View File
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Calculate cache key
id: cachekey
@@ -53,7 +53,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@v5
id: xtask-cache
with:
path: target/debug/xtask
+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"
+108 -47
View File
@@ -1,4 +1,4 @@
# Contributing to matrix-rust-sdk
# Contributing to `matrix-rust-sdk`
## Chat rooms
@@ -29,50 +29,55 @@ integration tests that need a running synapse instance. These tests reside in
[README](./testing/matrix-sdk-integration-testing/README.md) to easily set up a
synapse for testing purposes.
### Snapshot Testing
You can add/review snapshot tests using [insta.rs](https://insta.rs)
Every new struct/enum that derives `Serialize` `Deserialise` should have a snapshot test for it.
Any code change that breaks serialisation will then break a test, the author will then have to decide
how to handle migration and test it if needed.
Every new struct/enum that derives `Serialize` `Deserialise` should have a
snapshot test for it. Any code change that breaks serialisation will then break
a test, the author will then have to decide how to handle migration and test it
if needed.
And for an improved review experience it's recommended (but not necessary) to install the cargo-insta tool:
And for an improved review experience it's recommended (but not necessary) to
install the `cargo-insta` tool:
Unix:
```
```shell
curl -LsSf https://insta.rs/install.sh | sh
```
Windows:
```
```shell
powershell -c "irm https://insta.rs/install.ps1 | iex"
```
Usual flow is to first run the test, then review them.
```
```shell
cargo insta test
cargo insta review
```
### Intermittent failure policy
While we strive to add test coverage for as many features as we can, it sometimes happens that the
tests will be intermittently failing in CI (such tests are sometimes called "flaky"). This can be
caused by race conditions of all sorts, either in the test code itself, but sometimes in the
underlying feature being tested too, and as such, it requires some investigation, usually from the
original author of the test.
While we strive to add test coverage for as many features as we can, it
sometimes happens that the tests will be intermittently failing in CI (such
tests are sometimes called "flaky"). This can be caused by race conditions
of all sorts, either in the test code itself, but sometimes in the underlying
feature being tested too, and as such, it requires some investigation, usually
from the original author of the test.
Whenever such an intermittent failure happens, we try to open an issue to track the failures,
adding the
Whenever such an intermittent failure happens, we try to open an issue to track
the failures, adding the
[`intermittent-failure`](https://github.com/matrix-org/matrix-rust-sdk/issues?q=is%3Aissue%20state%3Aopen%20label%3Aintermittent-failure)
label to it, and commenting with links to CI runs where the failure happened.
If a test has been intermittently failing for **two weeks** or more, and no one is actively working
on fixing it, then we might decide to mark the test as `ignored` until it is fixed, to not cause
unrelated failures in other contributors' pull requests and pushes.
If a test has been intermittently failing for **two weeks** or more, and no one
is actively working on fixing it, then we might decide to mark the test as
`ignored` until it is fixed, to not cause unrelated failures in other
contributors' pull requests and pushes.
## Pull requests
@@ -87,7 +92,7 @@ be a good PR title.
(An additional bad example of a bad PR title would be `mynickname/branch name`,
that is, just the branch name.)
# Writing changelog entries
## Writing changelog entries
Our goal is to maintain clear, concise, and informative changelogs that
accurately document changes in the project. Changelog entries should be written
@@ -122,12 +127,17 @@ For security-related changelog entries, please include the following additional
details alongside the pull request number:
* Impact: Clearly describe the issue's potential impact on users or systems.
* CVE Number: If available, include the CVE (Common Vulnerabilities and Exposures) identifier.
* GitHub Advisory Link: Provide a link to the corresponding GitHub security advisory for further context.
* CVE Number: If available, include the CVE (Common Vulnerabilities and
Exposures) identifier.
* GitHub Advisory Link: Provide a link to the corresponding GitHub security
advisory for further context.
```markdown
- Use a constant-time Base64 encoder for secret key material to mitigate
side-channel attacks leaking secret key material ([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low, [CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640), [GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
side-channel attacks leaking secret key material
([#156](https://github.com/matrix-org/vodozemac/pull/156)) (Low,
[CVE-2024-40640](https://www.cve.org/CVERecord?id=CVE-2024-40640),
[GHSA-j8cm-g7r6-hfpq](https://github.com/matrix-org/vodozemac/security/advisories/GHSA-j8cm-g7r6-hfpq)).
```
## Commit message format
@@ -139,14 +149,15 @@ git trailers are supported and have special meaning (see below).
Conventional Commits are structured as follows:
```
```text
<type>(<scope>): <short summary>
```
The type of changes which will be included in changelogs is one of the following:
The type of changes which will be included in changelogs is one of the
following:
* `feat`: A new feature
* `fix`: A bug fix
* `fix`: A bugfix
* `doc`: Documentation changes
* `refactor`: Code refactoring
* `perf`: Performance improvements
@@ -163,15 +174,16 @@ changelog entry.
The metadata must be included in the following git-trailers:
* `Security-Impact`: The magnitude of harm that can be expected, i.e. low/moderate/high/critical.
* `Security-Impact`: The magnitude of harm that can be expected, i.e.
low/moderate/high/critical.
* `CVE`: The CVE that was assigned to this issue.
* `GitHub-Advisory`: The GitHub advisory identifier.
Please include all of the fields that are available.
Please include all the fields that are available.
Example:
```
```text
fix(crypto): Use a constant-time Base64 encoder for secret key material
This patch fixes a security issue around a side-channel vulnerability[1]
@@ -213,9 +225,9 @@ your contributions, follow these basic rules:
5. Keep PRs on topic and small. Large PRs are harder to review and more prone to
delays. Create small, focused commits that address a single topic. Use a
combination of [git add] -p or git checkout -p to split changes into logical
units. This makes your work easier to review and reduces the chance of
introducing unrelated changes.
combination of [git add] -p or [git checkout] -p to split changes into
logical units. This makes your work easier to review and reduces the chance
of introducing unrelated changes.
[git add]: https://git-scm.com/docs/git-add#Documentation/git-add.txt---patch
[git checkout]: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---patch
@@ -227,12 +239,12 @@ guidelines to make the maintainers life easier and increase the chances that
your PR will be reviewed swiftly.
1. Use [fixup] commits. When addressing reviewer feedback, you can create fixup
commits. These commits mark your changes as corrections of specific previous
commits in the PR.
commits. These commits mark your changes as corrections of specific previous
commits in the PR.
Example:
```bash
```shell
git commit --fixup=<commit-hash>
```
@@ -247,7 +259,7 @@ requested.
3. Once the PR has been approved, rebase your PR to squash all the fixup
commits, the [autosquash] option can help with this.
```bash
```shell
git rebase main --interactive --autosquash
```
@@ -257,14 +269,16 @@ git rebase main --interactive --autosquash
## Sign off
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the [Linux Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO ([Developer Certificate of Origin](http://developercertificate.org/)).
This is a simple declaration that you wrote the contribution or otherwise have the right
to contribute it to Matrix:
and you agree to license it under the same terms as the project's
license, we've adopted the same lightweight approach that the [Linux
Kernel](https://www.kernel.org/doc/Documentation/SubmittingPatches),
[Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md),
and many other projects use: the DCO ([Developer Certificate of
Origin](http://developercertificate.org/)). This is a simple declaration that
you wrote the contribution or otherwise have the right to contribute it to
Matrix:
```
```text
Developer Certificate of Origin
Version 1.1
@@ -305,7 +319,7 @@ By making a contribution to this project, I certify that:
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment:
```
```text
Signed-off-by: Your Name <your@email.example.org>
```
@@ -316,7 +330,7 @@ Git allows you to add this signoff automatically when using the `-s` flag to
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase:
```
```text
git rebase --signoff origin/main
```
@@ -324,8 +338,55 @@ git rebase --signoff origin/main
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
with all features enabled, causing an error in `matrix-sdk` ("only one of the
features 'native-tls' or 'rustls-tls' can be enabled"). To work around this,
features `native-tls` or `rustls-tls` can be enabled"). To work around this,
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
`native-tls` or `rustls-tls` feature definitions:
![Screenshot of RustRover](.img/rustrover-disable-feature.png)
## AI policy
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
### Terminology
This does not necessarily reflect the official or commonly used terminology.
Software and services that heavily rely on large language model technology to
generate their outcomes are referred to as _Artificial Intelligence_ (AI).
Examples of products that fit this definition: GitHub Copilot, ChatGPT, Claude
Sonnet, DeepSeek, Llama and Gemini.
There is a distinction between _general_ and _narrow_ AI, all the aforementioned
examples fall under general AI as they were not trained to execute a specific
well-defined task. Narrow AI is trained to be used for specific well-defined
tasks where the problem space is known in advance.
_Vibe coding_ is the practice where AI creates a code change (feature, bugfix,
tests, refactor) with a human that describes what needs to be implemented.
_AI agents_ are AIs that are configured to perform interactions or make changes
with little to no human supervision.
### Agreement
1. If content was made with the help of AI, you **must** convey that this is
the case. This includes content that you authored but was motivated by a
suggestion of AI.
2. If at any point you used AI's work in your contribution you should make
an effort to **verify** that you can submit this under the license of the
repository.
3. The **accountability** of using AI in a contribution lies with the person
that makes that contribution.
4. All communication, that includes: commit messages, pull request messages,
documentation, code comments and issues (and comments on issues/pull
requests), that is intended to be read by people to understand your thoughts
and work **must not** have been generated with AI. We exclude machine
translation and tooling that helps with grammar and spelling check.
5. Using general AI for review is **forbidden**. If the change contains changes
to the user experience it has to be approved by a human reviewer.
6. It is **not allowed** to use AI in an autonomous-looking way to contribute to
the Matrix Rust SDK. This also applies when someone engages in _vibe coding_
or uses so-called _agent mode_.
[Forgejo]: https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md
Generated
+727 -619
View File
File diff suppressed because it is too large Load Diff
+117 -84
View File
@@ -13,58 +13,65 @@ members = [
exclude = ["testing/data"]
# xtask, testing and the bindings should only be built when invoked explicitly.
default-members = ["benchmarks", "crates/*", "labs/*"]
resolver = "2"
resolver = "3"
[workspace.package]
rust-version = "1.88"
[workspace.dependencies]
anyhow = "1.0.99"
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.
async-stream = "0.3.5"
async-trait = "0.1.89"
base64 = "0.22.1"
bitflags = "2.9.3"
byteorder = "1.5.0"
cfg-if = "1.0.3"
clap = "4.5.46"
chrono = "0.4.41"
dirs = "6.0.0"
eyeball = { version = "0.8.8", features = ["tracing"] }
eyeball-im = { version = "0.7.0", features = ["tracing"] }
eyeball-im-util = "0.9.0"
futures-core = "0.3.31"
futures-executor = "0.3.31"
futures-util = "0.3.31"
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
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.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 = "5.0.0"
indexmap = "2.11.0"
insta = { version = "1.43.1", features = ["json", "redactions"] }
itertools = "0.14.0"
js-sys = "0.3.77"
mime = "0.3.17"
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 = { version = "0.14.0", default-features = false, features = ["use_std"] }
js-sys = { version = "0.3.82", default-features = false, features = ["std"] }
mime = { version = "0.3.17", default-features = false }
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest", "timing-resistant-secret-traits"] }
once_cell = "1.21.3"
pbkdf2 = { version = "0.12.2" }
pin-project-lite = "0.2.16"
once_cell = { version = "1.21.3", default-features = false }
pbkdf2 = { version = "0.12.2", default-features = false }
pin-project-lite = { version = "0.2.16", default-features = false }
proc-macro2 = { version = "1.0.106", default-features = false }
proptest = { version = "1.6.0", default-features = false, features = ["std"] }
rand = "0.8.5"
reqwest = { version = "0.12.23", default-features = false }
rmp-serde = "1.3.0"
ruma = { version = "0.13.0", features = [
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.12.24", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "289bee87974bd3c2ad14a6c15801c80b683b67dc", features = [
"client-api-c",
"compat-upload-signatures",
"compat-arbitrary-length-ids",
@@ -72,6 +79,7 @@ ruma = { version = "0.13.0", features = [
"compat-encrypted-stickers",
"compat-lax-room-create-deser",
"compat-lax-room-topic-deser",
"unstable-msc3230",
"unstable-msc3401",
"unstable-msc3488",
"unstable-msc3489",
@@ -79,54 +87,56 @@ ruma = { version = "0.13.0", features = [
"unstable-msc4140",
"unstable-msc4143",
"unstable-msc4171",
"unstable-msc4222",
"unstable-msc4278",
"unstable-msc4286",
"unstable-msc4306",
"unstable-msc4308"
"unstable-msc4308",
"unstable-msc4310",
] }
sentry = { version = "0.42.0", default-features = false }
sentry-tracing = "0.42.0"
serde = { version = "1.0.219", features = ["rc"] }
serde_html_form = "0.2.7"
serde_json = "1.0.143"
sha2 = "0.10.9"
similar-asserts = "1.7.0"
stream_assert = "0.1.1"
tempfile = "3.21.0"
thiserror = "2.0.16"
tokio = { version = "1.47.1", default-features = false, features = ["sync"] }
tokio-stream = "0.1.17"
sentry = { version = "0.46.0", default-features = false }
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 = { 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.24"
uniffi = { version = "0.28.0" }
uniffi_bindgen = { version = "0.28.0" }
url = "2.5.7"
uuid = "1.18.0"
vergen-gitcl = "1.0.8"
vodozemac = { version = "0.9.0", features = ["insecure-pk-encryption"] }
wasm-bindgen = "0.2.84"
wasm-bindgen-test = "0.3.50"
web-sys = "0.3.69"
wiremock = "0.6.5"
zeroize = "1.8.1"
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.14.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.14.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.14.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.14.0" }
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" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.16.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.16.0" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.14.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.14.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.14.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.14.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.14.0" }
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.14.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.14.0", default-features = false }
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.14.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.16.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.16.0" }
matrix-sdk-sqlite = { path = "crates/matrix-sdk-sqlite", version = "0.16.0", default-features = false }
matrix-sdk-store-encryption = { path = "crates/matrix-sdk-store-encryption", version = "0.16.0" }
matrix-sdk-test = { path = "testing/matrix-sdk-test", version = "0.16.0" }
matrix-sdk-test-utils = { path = "testing/matrix-sdk-test-utils", version = "0.16.0" }
matrix-sdk-ui = { path = "crates/matrix-sdk-ui", version = "0.16.0", default-features = false }
matrix-sdk-search = { path = "crates/matrix-sdk-search", version = "0.16.0" }
[workspace.lints.rust]
rust_2018_idioms = "warn"
@@ -164,6 +174,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.
@@ -184,6 +207,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.
+9 -2
View File
@@ -14,18 +14,21 @@ release = false
codspeed = []
[dependencies]
criterion = { version = "3.0.5", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
assert_matches.workspace = true
criterion = { version = "4.2.1", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
futures-util.workspace = true
matrix-sdk = { workspace = true, features = ["native-tls", "e2e-encryption", "sqlite", "testing"] }
matrix-sdk-base.workspace = true
matrix-sdk-crypto.workspace = true
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
matrix-sdk-test.workspace = true
matrix-sdk-ui.workspace = true
rand.workspace = true
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]]
@@ -51,3 +54,7 @@ harness = false
[[bench]]
name = "event_cache"
harness = false
[[bench]]
name = "room_list"
harness = false
+1
View File
@@ -317,6 +317,7 @@ fn find_event_relations(c: &mut Criterion) {
let (target, relations) = room_event_cache
.find_event_with_relations(target_event_id, filter)
.await
.unwrap()
.unwrap();
assert_eq!(target.event_id().as_deref().unwrap(), target_event_id);
assert_eq!(relations.len(), num_related_events as usize);
+3 -4
View File
@@ -181,15 +181,14 @@ pub fn load_pinned_events_benchmark(c: &mut Criterion) {
.lock()
.await
.unwrap()
.as_clean()
.unwrap()
.clear_all_linked_chunks()
.await
.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");
+90
View File
@@ -0,0 +1,90 @@
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_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 tokio::runtime::Builder;
/// Benchmark the time it takes to create a room list.
pub fn create(c: &mut Criterion) {
const NUMBER_OF_ROOMS: usize = 1000;
const NUMBER_OF_EVENTS_PER_ROOM: usize = 1000;
let runtime = Builder::new_multi_thread().enable_all().build().expect("Can't create runtime");
let (server, client) = runtime.block_on(async {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
client.event_cache().subscribe().unwrap();
(server, client)
});
let sender_id = owned_user_id!("@mnt_io:matrix.org");
let mut rand = rand::thread_rng();
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();
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()
})
.collect::<Vec<_>>();
let _room = runtime.block_on(async {
server
.sync_room(&client, JoinedRoomBuilder::new(&room_id).add_timeline_bulk(events))
.await
});
}
let mut group = c.benchmark_group("RoomList");
group.throughput(Throughput::Elements(NUMBER_OF_ROOMS.try_into().unwrap()));
group.bench_function(
BenchmarkId::new(
"Create",
format!("{NUMBER_OF_ROOMS} rooms × {NUMBER_OF_EVENTS_PER_ROOM} events"),
),
|bencher| {
bencher.to_async(&runtime).iter(|| async {
let room_list_service = RoomListService::new(client.clone())
.await
.expect("build the room list service");
let room_list = room_list_service.all_rooms().await.expect("fetch `all_rooms`");
let (entries_stream, entries_controller) =
room_list.entries_with_dynamic_adapters(20);
// Setting the filter will trigger the entries stream computation.
entries_controller.set_filter(Box::new(new_filter_non_left()));
pin_mut!(entries_stream);
let update = entries_stream.next().await.expect("receiving the reset update");
assert_eq!(update.len(), 1);
assert_matches!(&update[0], VectorDiff::Reset { values } => {
assert_eq!(values.len(), 20);
});
});
},
);
group.finish();
}
criterion_group! {
name = room_list;
config = Criterion::default();
targets = create
}
criterion_main!(room_list);
+2 -2
View File
@@ -1,7 +1,7 @@
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_ui::timeline::TimelineBuilder;
use matrix_sdk_ui::timeline::{TimelineBuilder, TimelineReadReceiptTracking};
use ruma::{
EventId, events::room::message::RoomMessageEventContentWithoutRelation, owned_room_id,
owned_user_id,
@@ -103,7 +103,7 @@ pub fn create_timeline_with_initial_events(c: &mut Criterion) {
|b| {
b.to_async(&runtime).iter(|| async {
let timeline = TimelineBuilder::new(&room)
.track_read_marker_and_receipts()
.track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
.build()
.await
.expect("Could not create timeline");
+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,
+41 -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
@@ -869,9 +872,18 @@ impl OlmMachine {
///
/// * `room_id` - The unique id of the room where the event was sent to.
///
/// * `handle_verification_events` - if the supplied event is a verification
/// event, use it to update the verification state. **Note**: it is
/// recommended to avoid setting this flag to true and use the explicit
/// [`OlmMachine::receive_verification_event`] method instead:
/// verification events sometimes need preparation before we can handle
/// them: see the documentation for
/// [`OlmMachine::receive_verification_event`].
///
/// * `strict_shields` - If `true`, messages will be decorated with strict
/// warnings (use `false` to match legacy behaviour where unsafe keys have
/// lower severity warnings and unverified identities are not decorated).
///
/// * `decryption_settings` - The setting for decrypting messages.
pub fn decrypt_room_event(
&self,
@@ -1100,6 +1112,14 @@ impl OlmMachine {
///
/// This method can be used to pass verification events that are happening
/// in rooms to the `OlmMachine`. The event should be in the decrypted form.
///
/// **Note**: If the supplied event is an `m.room.message` event with
/// `msgtype: m.key.verification.request`, then the device information for
/// the sending user must be up-to-date before calling this method
/// (otherwise, the request will be ignored). It is hard to guarantee this
/// is the case, but you can maximize your chances by explicitly making a
/// request to /keys/query for the user's device info, and processing the
/// response with [`OlmMachine::mark_request_as_sent`].
pub fn receive_verification_event(
&self,
event: String,
@@ -1392,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
quote.workspace = true
syn = { workspace = true, features = ["full", "extra-traits"] }
[lints]
workspace = true
+189 -2
View File
@@ -6,12 +6,199 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
### Bug Fixes
- `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
- 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))
### Refactor
- [**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
- `TimelineConfiguration::track_read_receipts`'s type is now an enum to allow tracking to be enabled for all events
(like before) or only for message-like events (which prevents read receipts from being placed on state events).
([#5900](https://github.com/matrix-org/matrix-rust-sdk/pull/5900))
- `Client::reset_server_info()` has been split into `reset_supported_versions()`
and `reset_well_known()`.
([#5910](https://github.com/matrix-org/matrix-rust-sdk/pull/5910))
- Add `HumanQrLoginError::NotFound` for non-existing / expired rendezvous sessions
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
- Add `HumanQrGrantLoginError::NotFound` for non-existing / expired rendezvous sessions
([#5898](https://github.com/matrix-org/matrix-rust-sdk/pull/5898))
- The `LatestEventValue::Local` type gains 2 new fields: `sender` and `profile`.
([#5885](https://github.com/matrix-org/matrix-rust-sdk/pull/5885))
- The `Encryption::user_identity()` method has received a new argument. The
`fallback_to_server` argument controls if we should attempt to fetch the user
identity from the homeserver if it wasn't found in the local storage.
([#5870](https://github.com/matrix-org/matrix-rust-sdk/pull/5870))
- Expose the power level required to modify `m.space.child` on
`room::power_levels::RoomPowerLevelsValues`.
- Rename `Client::login_with_qr_code` to `Client::new_login_with_qr_code_handler`.
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
- Add the `sqlite` feature, along with the `indexeddb` feature, to enable either
the SQLite or IndexedDB store. The `session_paths`, `session_passphrase`,
`session_pool_max_size`, `session_cache_size` and `session_journal_size_limit`
methods on `ClientBuilder` have been removed. New methods are added:
`ClientBuilder::in_memory_store` if one wants non-persistent stores,
`ClientBuilder::sqlite_store` to configure and to use SQLite stores (if
the `sqlite` feature is enabled), and `ClientBuilder::indexeddb_store` to
configure and to use IndexedDB stores (if the `indexeddb` feature is enabled).
([#5811](https://github.com/matrix-org/matrix-rust-sdk/pull/5811))
The code:
```rust
client_builder
.session_paths("data_path", "cache_path")
.passphrase("foobar")
```
now becomes:
```rust
client_builder
.sqlite_store(
SqliteSessionStoreBuilder::new("data_path", "cache_path")
.passphrase("foobar")
)
```
- UniFFI was upgraded to `v0.30.0` ([#5808](https://github.com/matrix-org/matrix-rust-sdk/pull/5808)).
- The `waveform` parameter in `Timeline::send_voice_message` format changed to a list of `f32`
between 0 and 1.
([#5732](https://github.com/matrix-org/matrix-rust-sdk/pull/5732))
- The `normalized_power_level` field has been removed from the `RoomMember`
struct.
([#5635](https://github.com/matrix-org/matrix-rust-sdk/pull/5635))
- Remove the deprecated `CallNotify` event (`org.matrix.msc4075.call.notify`) in favor of the new
`RtcNotification` event (`org.matrix.msc4075.rtc.notification`).
([#5668](https://github.com/matrix-org/matrix-rust-sdk/pull/5668))
- Add `QrLoginProgress::SyncingSecrets` to indicate that secrets are being synced between the two
devices.
([#5760](https://github.com/matrix-org/matrix-rust-sdk/pull/5760))
- Add `Room::subscribe_to_send_queue_updates` to observe room send queue updates.
([#5761](https://github.com/matrix-org/matrix-rust-sdk/pull/5761))
- `Client::login_with_qr_code` now returns a handler that allows performing the flow with either the
current device scanning or generating the QR code. Additionally, new errors `HumanQrLoginError::CheckCodeAlreadySent`
and `HumanQrLoginError::CheckCodeCannotBeSent` were added.
([#5786](https://github.com/matrix-org/matrix-rust-sdk/pull/5786))
- `ComposerDraft` now includes attachments alongside the text message.
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
- Add `Client::subscribe_to_send_queue_updates` to observe global send queue updates.
([#5784](https://github.com/matrix-org/matrix-rust-sdk/pull/5784))
### Features
- Add `Client::get_store_sizes()` so to query the size of the existing stores, if available. ([#5911](https://github.com/matrix-org/matrix-rust-sdk/pull/5911))
- Expose `is_space` in `NotificationRoomInfo`, allowing clients to determine if the room that triggered the notification is a space.
- Add push actions to `NotificationItem` and replace `SyncNotification` with `NotificationItem`.
([#5835](https://github.com/matrix-org/matrix-rust-sdk/pull/5835))
- Add `Client::new_grant_login_with_qr_code_handler` for granting login to a new device by way of
a QR code.
([#5836](https://github.com/matrix-org/matrix-rust-sdk/pull/5836))
- Add `Client::register_notification_handler` for observing notifications generated from sync responses.
([#5831](https://github.com/matrix-org/matrix-rust-sdk/pull/5831))
- Add `Room::mark_as_fully_read_unchecked` so clients can mark a room as read without needing a `Timeline` instance. Note this method is not recommended as it can potentially cause incorrect read receipts, but it can needed in certain cases.
- Add `Timeline::latest_event_id` to be able to fetch the event id of the latest event of the timeline.
- Add `Room::load_or_fetch_event` so we can get a `TimelineEvent` given its event id ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
- Add `TimelineEvent::thread_root_event_id` to expose the thread root event id for this type too ([#5678](https://github.com/matrix-org/matrix-rust-sdk/pull/5678)).
- Add `NotificationSettings::get_raw_push_rules` so clients can fetch the raw JSON content of the push rules of the current user and include it in bug reports ([#5706](https://github.com/matrix-org/matrix-rust-sdk/pull/5706)).
- Add new API to decline calls ([MSC4310](https://github.com/matrix-org/matrix-spec-proposals/pull/4310)): `Room::decline_call` and `Room::subscribe_to_call_decline_events`
([#5614](https://github.com/matrix-org/matrix-rust-sdk/pull/5614))
- Expose `m.federate` in `OtherState::RoomCreate` and `history_visibility` in `OtherState::RoomHistoryVisibility`, allowing clients to know whether a room federates and how its history is shared in the appropriate timeline events.
- Expose `join_rule` in `OtherState::RoomJoinRules`, allowing clients to know the join rules of a room from the appropriate timeline events.
### Changes
- `Timeline::latest_event_id` now uses its `ui::Timeline::latest_event_id` counterpart, instead of getting the latest event from the timeline and then its id.([#5864](https://github.com/matrix-org/matrix-rust-sdk/pull/5864))
- Build Android ARM64 bindings using better default RUSTFLAGS (the same used for iOS ARM64). This should improve performance. [(#5854)](https://github.com/matrix-org/matrix-rust-sdk/pull/5854)
## [0.14.0] - 2025-09-04
### Features:
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
rooms based on their low priority status. These filters allow clients to show only low priority rooms
- Add `LowPriority` and `NonLowPriority` variants to `RoomListEntriesDynamicFilterKind` for filtering
rooms based on their low priority status. These filters allow clients to show only low priority rooms
or exclude low priority rooms from the room list.
([#5508](https://github.com/matrix-org/matrix-rust-sdk/pull/5508))
- Add `room_version` and `privileged_creators_role` to `RoomInfo` ([#5449](https://github.com/matrix-org/matrix-rust-sdk/pull/5449)).
+46 -13
View File
@@ -1,6 +1,6 @@
[package]
name = "matrix-sdk-ffi"
version = "0.14.0"
version = "0.16.0"
edition = "2021"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ffi"]
@@ -24,8 +24,13 @@ crate-type = [
]
[features]
default = ["bundled-sqlite", "unstable-msc4274"]
bundled-sqlite = ["matrix-sdk/bundled-sqlite"]
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis"]
# Use SQLite for the session storage.
sqlite = ["matrix-sdk/sqlite"]
# Use an embedded version of SQLite.
bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
# Use IndexedDB for the session storage.
indexeddb = ["matrix-sdk/indexeddb"]
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
# Required when targeting a Javascript environment, like Wasm in a browser.
js = ["matrix-sdk-ui/js"]
@@ -36,34 +41,61 @@ rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
# Enable sentry error monitoring, not compatible with Wasm platforms.
sentry = ["dep:sentry", "dep:sentry-tracing"]
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
[dependencies]
anyhow.workspace = true
extension-trait = "1.0.1"
extension-trait = "1.0.2"
eyeball-im.workspace = true
futures-util.workspace = true
language-tags = "0.3.2"
log-panics = { version = "2", features = ["with-backtrace"] }
log-panics = { version = "2.1.0", default-features = false, features = ["with-backtrace"] }
matrix-sdk = { workspace = true, features = [
"anyhow",
"e2e-encryption",
"experimental-widgets",
"markdown",
"socks",
"sqlite",
"uniffi",
"federation-api",
] }
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.16"
mime = { version = "0.3.17", default-features = false }
once_cell.workspace = true
ruma = { workspace = true, features = ["html", "unstable-msc3488", "compat-unset-avatar", "unstable-msc3245-v1-compat", "unstable-msc4278", "unstable-hydra"] }
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",
@@ -75,14 +107,15 @@ 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
uniffi = { workspace = true, features = ["wasm-unstable-single-threaded"] }
futures-executor.workspace = true
[target.'cfg(not(target_family = "wasm"))'.dependencies]
async-compat.workspace = true
@@ -90,7 +123,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
uniffi = { workspace = true, features = ["tokio"] }
[target.'cfg(target_os = "android")'.dependencies]
paranoid-android = "0.2.1"
paranoid-android = { version = "0.2.2", default-features = false }
[dev-dependencies]
similar-asserts.workspace = true
+10 -8
View File
@@ -3,31 +3,33 @@
This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build the matrix bindings for native support and wasm-bindgen for web-browser assembly support. Please refer to the specific section to figure out how to build and use the bindings for your platform.
## Features
Given the number of platforms targeted, we have broken out a number of features
### Platform specific
### Platform specific
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
### Functionality
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
- `bundled-sqlite`: Use an embedded version of sqlite instead of the system provided one.
- `sqlite`: Use SQLite for the session storage.
- `bundled-sqlite`: Use an embedded version of SQLite instead of the system provided one.
- `indexeddb`: Use IndexedDB for the session storage.
### Unstable specs
- `unstable-msc4274`: Adds support for gallery message types, which contain multiple media elements.
## Platforms
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
- Javascript/Wasm: `"unstable-msc4274,native-tls"`
- JavaScript/Wasm: `"indexeddb,unstable-msc4274,native-tls"`
### Swift/iOS sync
### Swift/iOS async
TBD
+18
View File
@@ -43,6 +43,23 @@ fn setup_x86_64_android_workaround() {
}
}
/// Adds a workaround for watchOS simulator builds to manually link against the
/// CoreFoundation framework in order to avoid linker errors. Otherwise, errors
/// like the following may occur:
///
/// = note: Undefined symbols for architecture arm64:
/// "_CFArrayCreate", referenced from:
/// "_CFDataCreate", referenced from:
/// "_CFRelease", referenced from:
/// etc.
fn setup_watchos_simulator_workaround() {
let target = env::var("TARGET").expect("TARGET not set");
if target.ends_with("watchos-sim") {
println!("cargo:rustc-link-arg=-framework");
println!("cargo:rustc-link-arg=CoreFoundation");
}
}
/// Run the clang binary at `clang_path`, and return its major version number
fn get_clang_major_version(clang_path: &Path) -> String {
let clang_output =
@@ -58,6 +75,7 @@ fn get_clang_major_version(clang_path: &Path) -> String {
fn main() -> Result<(), Box<dyn Error>> {
setup_x86_64_android_workaround();
setup_watchos_simulator_workaround();
uniffi::generate_scaffolding("./src/api.udl").expect("Building the UDL file failed");
let git_config = GitclBuilder::default().sha(true).build()?;
+2
View File
@@ -1,10 +1,12 @@
namespace matrix_sdk_ffi {};
[Remote]
dictionary Mentions {
sequence<string> user_ids;
boolean room;
};
[Remote]
interface RoomMessageEventContentWithoutRelation {
RoomMessageEventContentWithoutRelation with_mentions(Mentions mentions);
};
+566 -142
View File
@@ -10,11 +10,14 @@ use anyhow::{anyhow, Context as _};
use futures_util::pin_mut;
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
#[cfg(feature = "sqlite")]
use matrix_sdk::STATE_STORE_DATABASE_NAME;
use matrix_sdk::{
authentication::oauth::{
AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession,
},
event_cache::EventCacheError,
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
executor::AbortOnDrop,
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
ruma::{
api::client::{
@@ -39,8 +42,8 @@ use matrix_sdk::{
},
sliding_sync::Version as SdkSlidingSyncVersion,
store::RoomLoadSettings as SdkRoomLoadSettings,
Account, AuthApi, AuthSession, Client as MatrixClient, SessionChange, SessionTokens,
STATE_STORE_DATABASE_NAME,
task_monitor::BackgroundTaskFailureReason,
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
};
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::{
@@ -58,6 +61,7 @@ use ruma::{
alias::get_alias,
error::ErrorKind,
profile::{AvatarUrl, DisplayName},
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
uiaa::UserIdentifier,
},
events::{
@@ -73,18 +77,18 @@ use ruma::{
join_rules::{
AllowRule as RumaAllowRule, JoinRule as RumaJoinRule, RoomJoinRulesEventContent,
},
message::OriginalSyncRoomMessageEvent,
power_levels::RoomPowerLevelsEventContent,
message::{OriginalSyncRoomMessageEvent, Relation},
},
secret_storage::{
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
},
tag::TagEventContent,
AnyMessageLikeEventContent, AnySyncTimelineEvent,
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
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};
@@ -101,10 +105,13 @@ use crate::{
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
client,
encryption::Encryption,
notification::NotificationClient,
notification::{
NotificationClient, NotificationEvent, NotificationItem, NotificationRoomInfo,
NotificationSenderInfo,
},
notification_settings::NotificationSettings,
qr_code::{HumanQrLoginError, QrCodeData, QrLoginProgressListener},
room::{RoomHistoryVisibility, RoomInfoListener},
qr_code::{GrantLoginWithQrCodeHandler, LoginWithQrCodeHandler},
room::{RoomHistoryVisibility, RoomInfoListener, RoomSendQueueUpdate},
room_directory_search::RoomDirectorySearch,
room_preview::RoomPreview,
ruma::{
@@ -182,7 +189,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)]
@@ -196,6 +215,13 @@ pub trait ProgressWatcher: SyncOutsideWasm + SendOutsideWasm {
fn transmission_progress(&self, progress: TransmissionProgress);
}
/// A listener to the global (client-wide) update reporter of the send queue.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueRoomUpdateListener: SyncOutsideWasm + SendOutsideWasm {
/// Called every time the send queue emits an update for a given room.
fn on_update(&self, room_id: String, update: RoomSendQueueUpdate);
}
/// A listener to the global (client-wide) error reporter of the send queue.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueRoomErrorListener: SyncOutsideWasm + SendOutsideWasm {
@@ -218,6 +244,16 @@ pub trait RoomAccountDataListener: SyncOutsideWasm + SendOutsideWasm {
fn on_change(&self, event: RoomAccountDataEvent, room_id: String);
}
/// A listener for notifications generated from sync responses.
///
/// This is called during sync for each event that triggers a notification
/// based on the user's push rules.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SyncNotificationListener: SyncOutsideWasm + SendOutsideWasm {
/// Called when a notifying event is received during sync.
fn on_notification(&self, notification: NotificationItem, room_id: String);
}
#[derive(Clone, Copy, uniffi::Record)]
pub struct TransmissionProgress {
pub current: u64,
@@ -233,16 +269,30 @@ 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>>,
session_verification_controller:
Arc<tokio::sync::RwLock<Option<SessionVerificationController>>>,
/// The path to the directory where the state store and the crypto store are
/// located, if the `Client` instance has been built with a SQLite store
/// backend.
/// located, if the `Client` instance has been built with a store (either
/// SQLite or IndexedDB).
#[cfg_attr(not(feature = "sqlite"), allow(unused))]
store_path: Option<PathBuf>,
}
@@ -286,7 +336,7 @@ impl Client {
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,
@@ -332,6 +382,17 @@ impl Client {
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Perform database optimizations if any are available, i.e. vacuuming in
/// SQLite.
pub async fn optimize_stores(&self) -> Result<(), ClientError> {
Ok(self.inner.optimize_stores().await?)
}
/// Returns the sizes of the existing stores, if known.
pub async fn get_store_sizes(&self) -> Result<StoreSizes, ClientError> {
Ok(self.inner.get_store_sizes().await?.into())
}
/// Information about login options for the client's homeserver.
pub async fn homeserver_login_details(&self) -> Arc<HomeserverLoginDetails> {
let oauth = self.inner.oauth();
@@ -544,43 +605,24 @@ impl Client {
Ok(())
}
/// Log in using the provided [`QrCodeData`]. The `Client` must be built
/// by providing [`QrCodeData::server_name`] as the server name for this
/// login to succeed.
/// Create a handler for requesting an existing device to grant login to
/// this device by way of a QR code.
///
/// This method uses the login mechanism described in [MSC4108]. As such
/// this method requires OAuth 2.0 support as well as sliding sync support.
/// # Arguments
///
/// The usage of the progress_listener is required to transfer the
/// [`CheckCode`] to the existing client.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn login_with_qr_code(
/// * `oidc_configuration` - The data to restore or register the client with
/// the server.
pub fn new_login_with_qr_code_handler(
self: Arc<Self>,
qr_code_data: &QrCodeData,
oidc_configuration: &OidcConfiguration,
progress_listener: Box<dyn QrLoginProgressListener>,
) -> Result<(), HumanQrLoginError> {
let registration_data = oidc_configuration
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
oidc_configuration: OidcConfiguration,
) -> LoginWithQrCodeHandler {
LoginWithQrCodeHandler::new(self.inner.oauth(), oidc_configuration)
}
let oauth = self.inner.oauth();
let login = oauth.login_with_qr_code(&qr_code_data.inner, Some(&registration_data));
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
Ok(())
/// Create a handler for granting login from this device to a new device by
/// way of a QR code.
pub fn new_grant_login_with_qr_code_handler(self: Arc<Self>) -> GrantLoginWithQrCodeHandler {
GrantLoginWithQrCodeHandler::new(self.inner.oauth())
}
/// Restores the client from a `Session`.
@@ -634,6 +676,49 @@ impl Client {
self.inner.send_queue().enable_upload_progress(enable);
}
/// Subscribe to the global send queue update reporter, at the
/// client-wide level.
///
/// The given listener will be immediately called with
/// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
/// the queue.
pub async fn subscribe_to_send_queue_updates(
&self,
listener: Box<dyn SendQueueRoomUpdateListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let q = self.inner.send_queue();
let local_echoes = q.local_echoes().await?;
let mut subscriber = q.subscribe();
for (room_id, local_echoes) in local_echoes {
for local_echo in local_echoes {
listener.on_update(
room_id.clone().into(),
RoomSendQueueUpdate::NewLocalEvent {
transaction_id: local_echo.transaction_id.into(),
},
);
}
}
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
loop {
match subscriber.recv().await {
Ok(update) => {
let room_id = update.room_id.to_string();
match update.update.try_into() {
Ok(update) => listener.on_update(room_id, update),
Err(err) => error!("error when converting send queue update: {err}"),
}
}
Err(err) => {
error!("error when listening to the send queue update reporter: {err}");
}
}
}
}))))
}
/// Subscribe to the global enablement status of the send queue, at the
/// client-wide level.
///
@@ -789,6 +874,158 @@ impl Client {
}
}
/// Register a handler for notifications generated from sync responses.
///
/// The handler will be called during sync for each event that triggers
/// a notification based on the user's push rules.
///
/// The handler receives:
/// - The notification with push actions and event data
/// - The room ID where the notification occurred
///
/// This is useful for implementing custom notification logic, such as
/// displaying local notifications or updating notification badges.
pub async fn register_notification_handler(&self, listener: Box<dyn SyncNotificationListener>) {
let listener = Arc::new(listener);
self.inner
.register_notification_handler(move |notification, room, _client| {
let listener = listener.clone();
let room_id = room.room_id().to_string();
async move {
// Extract information about the actions
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
// Convert SDK actions to FFI type
let actions: Vec<crate::notification_settings::Action> = notification
.actions
.into_iter()
.filter_map(|action| action.try_into().ok())
.collect();
// Convert SDK event to FFI type
let (sender, event, thread_id, raw_event) = match notification.event {
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let thread_id = match &deserialized {
AnySyncTimelineEvent::MessageLike(event) => {
match event.original_content() {
Some(AnyMessageLikeEventContent::RoomMessage(
content,
)) => match content.relates_to {
Some(Relation::Thread(thread)) => {
Some(thread.event_id.to_string())
}
_ => None,
},
_ => None,
}
}
_ => None,
};
let event = NotificationEvent::Timeline {
event: Arc::new(crate::event::TimelineEvent(Box::new(
deserialized,
))),
};
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!("Failed to deserialize timeline event: {err}");
return;
}
}
}
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
let raw_event = raw.json().get().to_owned();
match raw.deserialize() {
Ok(deserialized) => {
let sender = deserialized.sender().to_owned();
let event =
NotificationEvent::Invite { sender: sender.to_string() };
let thread_id = None;
(sender, event, thread_id, raw_event)
}
Err(err) => {
tracing::warn!(
"Failed to deserialize stripped state event: {err}"
);
return;
}
}
}
};
// Compile sender info
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
let sender_info = if let Some(sender) = sender.as_ref() {
NotificationSenderInfo {
display_name: sender.display_name().map(|name| name.to_owned()),
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
is_name_ambiguous: sender.name_ambiguous(),
}
} else {
NotificationSenderInfo {
display_name: None,
avatar_url: None,
is_name_ambiguous: false,
}
};
// Compile room info
let display_name = match room.display_name().await {
Ok(name) => name.to_string(),
Err(err) => {
tracing::warn!("Failed to calculate the room's display name: {err}");
return;
}
};
let is_direct = match room.is_direct().await {
Ok(is_direct) => is_direct,
Err(err) => {
tracing::warn!("Failed to determine if room is direct or not: {err}");
return;
}
};
let room_info = NotificationRoomInfo {
display_name,
avatar_url: room.avatar_url().map(Into::into),
canonical_alias: room.canonical_alias().map(Into::into),
topic: room.topic(),
join_rule: room
.join_rule()
.map(TryInto::try_into)
.transpose()
.ok()
.flatten(),
joined_members_count: room.joined_members_count(),
is_encrypted: Some(room.encryption_state().is_encrypted()),
is_direct,
is_space: room.is_space(),
};
listener.on_notification(
NotificationItem {
event,
raw_event,
sender_info,
room_info,
is_noisy: Some(is_noisy),
has_mention: Some(has_mention),
thread_id,
actions: Some(actions),
},
room_id,
);
}
})
.await;
}
/// Allows generic GET requests to be made through the SDK's internal HTTP
/// client. This is useful when the caller's native HTTP client wouldn't
/// have the same configuration (such as certificates, proxies, etc.) This
@@ -811,15 +1048,23 @@ impl Client {
/// Empty the server version and unstable features cache.
///
/// Since the SDK caches server info (versions, unstable features,
/// well-known etc), it's possible to have a stale entry in the cache.
/// This functions makes it possible to force reset it.
pub async fn reset_server_info(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_server_info().await?)
/// Since the SDK caches the supported versions, it's possible to have a
/// stale entry in the cache. This functions makes it possible to force
/// reset it.
pub async fn reset_supported_versions(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_supported_versions().await?)
}
/// Empty the well-known cache.
///
/// Since the SDK caches the well-known, it's possible to have a stale
/// entry in the cache. This functions makes it possible to force reset
/// it.
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
Ok(self.inner.reset_well_known().await?)
}
}
#[cfg(not(target_family = "wasm"))]
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Retrieves a media file from the media source
@@ -833,22 +1078,60 @@ impl Client {
use_cache: bool,
temp_dir: Option<String>,
) -> Result<Arc<MediaFileHandle>, ClientError> {
let source = (*media_source).clone();
let mime_type: mime::Mime = mime_type.parse()?;
#[cfg(not(target_family = "wasm"))]
{
let source = (*media_source).clone();
let mime_type: mime::Mime = mime_type.parse()?;
let handle = self
.inner
.media()
.get_media_file(
&MediaRequestParameters { source: source.media_source, format: MediaFormat::File },
filename,
&mime_type,
use_cache,
temp_dir,
)
.await?;
let handle = self
.inner
.media()
.get_media_file(
&MediaRequestParameters {
source: source.media_source,
format: MediaFormat::File,
},
filename,
&mime_type,
use_cache,
temp_dir,
)
.await?;
Ok(Arc::new(MediaFileHandle::new(handle)))
Ok(Arc::new(MediaFileHandle::new(handle)))
}
/// MediaFileHandle uses SdkMediaFileHandle which requires an
/// intermediate TempFile which is not available on wasm
/// platforms due to lack of an accessible file system.
#[cfg(target_family = "wasm")]
Err(ClientError::Generic {
msg: "get_media_file is not supported on wasm platforms".to_owned(),
details: None,
})
}
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
#[cfg(not(target_family = "wasm"))]
{
self.inner
.account()
.set_display_name(Some(name.as_str()))
.await
.context("Unable to set display name")?;
}
#[cfg(target_family = "wasm")]
{
self.inner.account().set_display_name(Some(name.as_str())).await.map_err(|e| {
ClientError::Generic {
msg: "Unable to set display name".to_owned(),
details: Some(e.to_string()),
}
})?;
}
Ok(())
}
}
@@ -876,14 +1159,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 {
@@ -899,13 +1182,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>,
@@ -984,15 +1283,6 @@ impl Client {
Ok(display_name)
}
pub async fn set_display_name(&self, name: String) -> Result<(), ClientError> {
self.inner
.account()
.set_display_name(Some(name.as_str()))
.await
.context("Unable to set display name")?;
Ok(())
}
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
let mime: Mime = mime_type.parse()?;
self.inner.account().upload_avatar(&mime, data).await?;
@@ -1259,8 +1549,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))
}
@@ -1510,8 +1800,8 @@ impl Client {
&self,
policy: MediaRetentionPolicy,
) -> Result<(), ClientError> {
let closure = async || -> Result<_, EventCacheError> {
let store = self.inner.event_cache_store().lock().await?;
let closure = async || -> Result<_, Error> {
let store = self.inner.media_store().lock().await?;
Ok(store.set_media_retention_policy(policy).await?)
};
@@ -1559,13 +1849,13 @@ impl Client {
// Clean up the media cache according to the current media retention policy.
self.inner
.event_cache_store()
.media_store()
.lock()
.await
.map_err(EventCacheError::from)?
.clean_up_media_cache()
.map_err(Error::from)?
.clean()
.await
.map_err(EventCacheError::from)?;
.map_err(Error::from)?;
// Clear all the room chunks. It's important to *not* call
// `EventCacheStore::clear_all_linked_chunks` here, because there might be live
@@ -1574,6 +1864,7 @@ impl Client {
self.inner.event_cache().clear_all_rooms().await?;
// Delete the state store file, if it exists.
#[cfg(feature = "sqlite")]
if let Some(store_path) = &self.store_path {
debug!("Removing the state store: {}", store_path.display());
@@ -1623,6 +1914,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
@@ -1748,6 +2045,42 @@ impl Client {
}
}
#[cfg(feature = "experimental-element-recent-emojis")]
mod recent_emoji {
use crate::{client::Client, error::ClientError};
/// Represents an emoji recently used for reactions.
#[derive(Debug, uniffi::Record)]
pub struct RecentEmoji {
/// The actual emoji text representation.
pub emoji: String,
/// The number of times this emoji has been used for reactions.
pub count: u64,
}
#[matrix_sdk_ffi_macros::export]
impl Client {
/// Adds a recently used emoji to the list and uploads the updated
/// `io.element.recent_emoji` content to the global account data.
pub async fn add_recent_emoji(&self, emoji: String) -> Result<(), ClientError> {
Ok(self.inner.account().add_recent_emoji(&emoji).await?)
}
/// Gets the list of recently used emojis from the
/// `io.element.recent_emoji` global account data.
pub async fn get_recent_emojis(&self) -> Result<Vec<RecentEmoji>, ClientError> {
Ok(self
.inner
.account()
.get_recent_emojis(false)
.await?
.into_iter()
.map(|(emoji, count)| RecentEmoji { emoji, count: count.into() })
.collect::<Vec<RecentEmoji>>())
}
}
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait MediaPreviewConfigListener: SyncOutsideWasm + SendOutsideWasm {
fn on_change(&self, media_preview_config: Option<MediaPreviewConfig>);
@@ -1841,8 +2174,9 @@ 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);
@@ -1941,34 +2275,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()
@@ -1980,7 +2297,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
}
})
.collect();
power_levels.events = value
.events
.iter()
@@ -1989,7 +2305,6 @@ impl From<PowerLevels> for RoomPowerLevelsEventContent {
(event_type, (*power_level).into())
})
.collect();
power_levels
}
}
@@ -2016,6 +2331,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 {
@@ -2048,30 +2365,36 @@ impl TryFrom<CreateRoomParameters> for create_room::v3::Request {
if value.is_encrypted {
let content =
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
}
if let Some(url) = value.avatar {
let mut content = RoomAvatarEventContent::new();
content.url = Some(url.into());
initial_state.push(InitialStateEvent::new(content).to_raw_any());
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
}
if let Some(join_rule_override) = value.join_rule_override {
let content = RoomJoinRulesEventContent::new(join_rule_override.try_into()?);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
}
if let Some(history_visibility_override) = value.history_visibility_override {
let content =
RoomHistoryVisibilityEventContent::new(history_visibility_override.try_into()?);
initial_state.push(InitialStateEvent::new(content).to_raw_any());
initial_state.push(InitialStateEvent::with_empty_state_key(content).to_raw_any());
}
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);
}
@@ -2307,25 +2630,25 @@ fn gen_transaction_id() -> String {
/// A file handle that takes ownership of a media file on disk. When the handle
/// is dropped, the file will be removed from the disk.
#[cfg(not(target_family = "wasm"))]
#[derive(uniffi::Object)]
pub struct MediaFileHandle {
#[cfg(not(target_family = "wasm"))]
inner: std::sync::RwLock<Option<SdkMediaFileHandle>>,
}
#[cfg(not(target_family = "wasm"))]
impl MediaFileHandle {
#[cfg(not(target_family = "wasm"))]
fn new(handle: SdkMediaFileHandle) -> Self {
Self { inner: std::sync::RwLock::new(Some(handle)) }
}
}
#[cfg(not(target_family = "wasm"))]
#[matrix_sdk_ffi_macros::export]
impl MediaFileHandle {
/// Get the media file's path.
pub fn path(&self) -> Result<String, ClientError> {
Ok(self
#[cfg(not(target_family = "wasm"))]
return Ok(self
.inner
.read()
.unwrap()
@@ -2334,24 +2657,37 @@ impl MediaFileHandle {
.path()
.to_str()
.unwrap()
.to_owned())
.to_owned());
#[cfg(target_family = "wasm")]
Err(ClientError::Generic {
msg: "MediaFileHandle.path() is not supported on WASM targets".to_string(),
details: None,
})
}
pub fn persist(&self, path: String) -> Result<bool, ClientError> {
let mut guard = self.inner.write().unwrap();
Ok(
match guard
.take()
.context("MediaFileHandle was already persisted")?
.persist(path.as_ref())
{
Ok(_) => true,
Err(e) => {
*guard = Some(e.file);
false
}
},
)
#[cfg(not(target_family = "wasm"))]
{
let mut guard = self.inner.write().unwrap();
Ok(
match guard
.take()
.context("MediaFileHandle was already persisted")?
.persist(path.as_ref())
{
Ok(_) => true,
Err(e) => {
*guard = Some(e.file);
false
}
},
)
}
#[cfg(target_family = "wasm")]
Err(ClientError::Generic {
msg: "MediaFileHandle.persist() is not supported on WASM targets".to_string(),
details: None,
})
}
}
@@ -2566,3 +2902,91 @@ impl TryFrom<RumaAllowRule> for AllowRule {
}
}
}
/// Contains the disk size of the different stores, if known. It won't be
/// available for in-memory stores.
#[derive(Debug, Clone, uniffi::Record)]
pub struct StoreSizes {
/// The size of the CryptoStore.
crypto_store: Option<u64>,
/// The size of the StateStore.
state_store: Option<u64>,
/// The size of the EventCacheStore.
event_cache_store: Option<u64>,
/// The size of the MediaStore.
media_store: Option<u64>,
}
impl From<matrix_sdk::StoreSizes> for StoreSizes {
fn from(value: matrix_sdk::StoreSizes) -> Self {
Self {
crypto_store: value.crypto_store.map(|v| v as u64),
state_store: value.state_store.map(|v| v as u64),
event_cache_store: value.event_cache_store.map(|v| v as u64),
media_store: value.media_store.map(|v| v as u64),
}
}
}
#[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));
}
}
+108 -155
View File
@@ -1,9 +1,11 @@
use std::{fs, num::NonZeroUsize, path::Path, sync::Arc, time::Duration};
// Allow UniFFI to use methods marked as `#[deprecated]`.
#![allow(deprecated)]
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
#[cfg(not(target_family = "wasm"))]
use matrix_sdk::reqwest::Certificate;
use matrix_sdk::{
crypto::{CollectStrategy, DecryptionSettings, TrustRequirement},
encryption::{BackupDownloadStrategy, EncryptionSettings},
event_cache::EventCacheError,
ruma::{ServerName, UserId},
@@ -12,14 +14,21 @@ use matrix_sdk::{
VersionBuilderError,
},
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
RumaApiError, SqliteStoreConfig, ThreadingSupport,
RumaApiError, ThreadingSupport,
};
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
use ruma::api::error::{DeserializationError, FromHttpResponseError};
use tracing::{debug, error};
use zeroize::Zeroizing;
use tracing::debug;
use super::client::Client;
use crate::{client::ClientSessionDelegate, error::ClientError, helpers::unwrap_or_clone_arc};
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
use crate::store;
use crate::{
client::ClientSessionDelegate,
error::ClientError,
helpers::unwrap_or_clone_arc,
store::{StoreBuilder, StoreBuilderOutcome},
};
/// A list of bytes containing a certificate in DER or PEM form.
pub type CertificateBytes = Vec<u8>;
@@ -100,11 +109,7 @@ impl From<ClientError> for ClientBuildError {
#[derive(Clone, uniffi::Object)]
pub struct ClientBuilder {
session_paths: Option<SessionPaths>,
session_passphrase: Zeroizing<Option<String>>,
session_pool_max_size: Option<usize>,
session_cache_size: Option<u32>,
session_journal_size_limit: Option<u32>,
store: Option<StoreBuilder>,
system_is_memory_constrained: bool,
username: Option<String>,
homeserver_cfg: Option<HomeserverConfig>,
@@ -143,23 +148,24 @@ impl ClientBuilder {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {
session_paths: None,
session_passphrase: Zeroizing::new(None),
session_pool_max_size: None,
session_cache_size: None,
session_journal_size_limit: None,
store: None,
system_is_memory_constrained: false,
username: None,
homeserver_cfg: None,
#[cfg(not(target_family = "wasm"))]
user_agent: None,
sliding_sync_version_builder: SlidingSyncVersionBuilder::None,
#[cfg(not(target_family = "wasm"))]
proxy: None,
#[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,
session_delegate: None,
#[cfg(not(target_family = "wasm"))]
additional_root_certificates: Default::default(),
#[cfg(not(target_family = "wasm"))]
disable_built_in_root_certificates: false,
encryption_settings: EncryptionSettings {
auto_enable_cross_signing: false,
@@ -201,80 +207,13 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Sets the paths that the client will use to store its data and caches.
/// Both paths **must** be unique per session as the SDK stores aren't
/// capable of handling multiple users, however it is valid to use the
/// same path for both stores on a single session.
///
/// Leaving this unset tells the client to use an in-memory data store.
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_paths = Some(SessionPaths { data_path, cache_path });
Arc::new(builder)
}
/// Set the passphrase for the stores given to
/// [`ClientBuilder::session_paths`].
pub fn session_passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_passphrase = Zeroizing::new(passphrase);
Arc::new(builder)
}
/// Set the pool max size for the SQLite stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store exposes an async pool of connections. This method controls
/// the size of the pool. The larger the pool is, the more memory is
/// consumed, but also the more the app is reactive because it doesn't need
/// to wait on a pool to be available to run queries.
///
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
pub fn session_pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_pool_max_size = pool_max_size
.map(|size| size.try_into().expect("`pool_max_size` is too large to fit in `usize`"));
Arc::new(builder)
}
/// Set the cache size for the SQLite stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store exposes a SQLite connection. This method controls the cache
/// size, in **bytes (!)**.
///
/// The cache represents data SQLite holds in memory at once per open
/// database file. The default cache implementation does not allocate the
/// full amount of cache memory all at once. Cache memory is allocated
/// in smaller chunks on an as-needed basis.
///
/// See [`SqliteStoreConfig::cache_size`] to learn more.
pub fn session_cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_cache_size = cache_size;
Arc::new(builder)
}
/// Set the size limit for the SQLite WAL files of stores given to
/// [`ClientBuilder::session_paths`].
///
/// Each store uses the WAL journal mode. This method controls the size
/// limit of the WAL files, in **bytes (!)**.
///
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
pub fn session_journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.session_journal_size_limit = limit;
Arc::new(builder)
}
/// Tell the client that the system is memory constrained, like in a push
/// notification process for example.
///
/// So far, at the time of writing (2025-04-07), it changes the defaults of
/// [`SqliteStoreConfig`], so one might not need to call
/// [`ClientBuilder::session_cache_size`] and siblings for example. Please
/// check [`SqliteStoreConfig::with_low_memory_config`].
/// `matrix_sdk::SqliteStoreConfig` (if the `sqlite` feature is enabled).
/// Please check
/// `matrix_sdk::SqliteStoreConfig::with_low_memory_config`.
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.system_is_memory_constrained = true;
@@ -404,6 +343,13 @@ impl ClientBuilder {
Arc::new(builder)
}
/// Use in-memory session storage.
pub fn in_memory_store(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.store = Some(StoreBuilder::InMemory);
Arc::new(builder)
}
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
let builder = unwrap_or_clone_arc(self);
let mut inner_builder = MatrixClient::builder();
@@ -413,48 +359,26 @@ impl ClientBuilder {
inner_builder.cross_process_store_locks_holder_name(holder_name.clone());
}
let store_path = if let Some(session_paths) = &builder.session_paths {
// This is the path where both the state store and the crypto store will live.
let data_path = Path::new(&session_paths.data_path);
// This is the path where the event cache store will live.
let cache_path = Path::new(&session_paths.cache_path);
let store_path = if let Some(store) = &builder.store {
match store.build()? {
#[cfg(feature = "sqlite")]
StoreBuilderOutcome::Sqlite { config, cache_path, store_path: data_path } => {
inner_builder = inner_builder
.sqlite_store_with_config_and_cache_path(config, Some(cache_path));
debug!(
data_path = %data_path.to_string_lossy(),
event_cache_path = %cache_path.to_string_lossy(),
"Creating directories for data (state and crypto) and cache stores.",
);
Some(data_path)
}
#[cfg(feature = "indexeddb")]
StoreBuilderOutcome::IndexedDb { name, passphrase } => {
inner_builder = inner_builder.indexeddb_store(&name, passphrase.as_deref());
fs::create_dir_all(data_path)?;
fs::create_dir_all(cache_path)?;
None
}
let mut sqlite_store_config = if builder.system_is_memory_constrained {
SqliteStoreConfig::with_low_memory_config(data_path)
} else {
SqliteStoreConfig::new(data_path)
};
sqlite_store_config =
sqlite_store_config.passphrase(builder.session_passphrase.as_deref());
if let Some(size) = builder.session_pool_max_size {
sqlite_store_config = sqlite_store_config.pool_max_size(size);
StoreBuilderOutcome::InMemory => None,
}
if let Some(size) = builder.session_cache_size {
sqlite_store_config = sqlite_store_config.cache_size(size);
}
if let Some(limit) = builder.session_journal_size_limit {
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
}
inner_builder = inner_builder
.sqlite_store_with_config_and_cache_path(sqlite_store_config, Some(cache_path));
Some(data_path.to_owned())
} else {
debug!("Not using a store path.");
debug!("Not using a session store");
None
};
@@ -574,21 +498,6 @@ impl ClientBuilder {
let sdk_client = inner_builder.build().await?;
// Disable retries for this request to prevent it from being retried
// indefinitely
let config = sdk_client.request_config().disable_retry();
// Log server version information at info level.
if let Ok(server_info) = sdk_client.server_vendor_info(Some(config)).await {
tracing::info!(
server_name = %server_info.server_name,
version = %server_info.version,
"Connected to Matrix server"
);
} else {
tracing::warn!("Could not retrieve server version information");
}
Ok(Arc::new(
Client::new(
sdk_client,
@@ -601,18 +510,62 @@ impl ClientBuilder {
}
}
#[cfg(not(target_family = "wasm"))]
#[cfg(feature = "sqlite")]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
/// Use SQLite as the session storage.
pub fn sqlite_store(self: Arc<Self>, config: Arc<store::SqliteStoreBuilder>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.store = Some(StoreBuilder::Sqlite(unwrap_or_clone_arc(config)));
Arc::new(builder)
}
/// Sets the paths that the client will use to store its data and caches
/// with SQLite.
///
/// Both paths **must** be unique per session as the SDK
/// stores aren't capable of handling multiple users, however it is
/// valid to use the same path for both stores on a single session.
#[deprecated = "Use `ClientBuilder::session_store_with_sqlite` instead"]
pub fn session_paths(self: Arc<Self>, data_path: String, cache_path: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.store =
Some(StoreBuilder::Sqlite(store::SqliteStoreBuilder::raw_new(data_path, cache_path)));
Arc::new(builder)
}
}
#[cfg(feature = "indexeddb")]
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
/// Use IndexedDB as the session storage.
pub fn indexeddb_store(
self: Arc<Self>,
config: Arc<store::IndexedDbStoreBuilder>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.store = Some(StoreBuilder::IndexedDb(unwrap_or_clone_arc(config)));
Arc::new(builder)
}
}
#[matrix_sdk_ffi_macros::export]
impl ClientBuilder {
pub fn proxy(self: Arc<Self>, url: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.proxy = Some(url);
#[cfg(not(target_family = "wasm"))]
{
builder.proxy = Some(url);
}
Arc::new(builder)
}
pub fn disable_ssl_verification(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_ssl_verification = true;
#[cfg(not(target_family = "wasm"))]
{
builder.disable_ssl_verification = true;
}
Arc::new(builder)
}
@@ -621,7 +574,11 @@ impl ClientBuilder {
certificates: Vec<CertificateBytes>,
) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.additional_root_certificates = certificates;
#[cfg(not(target_family = "wasm"))]
{
builder.additional_root_certificates = certificates;
}
Arc::new(builder)
}
@@ -631,29 +588,25 @@ impl ClientBuilder {
/// [`add_root_certificates`][ClientBuilder::add_root_certificates].
pub fn disable_built_in_root_certificates(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.disable_built_in_root_certificates = true;
#[cfg(not(target_family = "wasm"))]
{
builder.disable_built_in_root_certificates = true;
}
Arc::new(builder)
}
pub fn user_agent(self: Arc<Self>, user_agent: String) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.user_agent = Some(user_agent);
#[cfg(not(target_family = "wasm"))]
{
builder.user_agent = Some(user_agent);
}
Arc::new(builder)
}
}
/// The store paths the client will use when built.
#[derive(Clone)]
struct SessionPaths {
/// The path that the client will use to store its data.
data_path: String,
/// The path that the client will use to store its caches. This path can be
/// the same as the data path if you prefer to keep everything in one place.
cache_path: String,
}
#[derive(Clone, uniffi::Record)]
/// The config to use for HTTP requests by default in this client.
#[derive(Clone, uniffi::Record)]
pub struct RequestConfig {
/// Max number of retries.
retry_limit: Option<u64>,
+28 -4
View File
@@ -79,9 +79,14 @@ pub enum RecoveryError {
#[error(transparent)]
Client { source: crate::ClientError },
/// Error in the secret storage subsystem.
/// Error in the secret storage subsystem, except for when importing a
/// secret.
#[error("Error in the secret-storage subsystem: {error_message}")]
SecretStorage { error_message: String },
/// Error when importing a secret from secret storage.
#[error("Error importing a secret: {error_message}")]
Import { error_message: String },
}
impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
@@ -89,6 +94,9 @@ impl From<matrix_sdk::encryption::recovery::RecoveryError> for RecoveryError {
match value {
recovery::RecoveryError::BackupExistsOnServer => Self::BackupExistsOnServer,
recovery::RecoveryError::Sdk(e) => Self::Client { source: ClientError::from(e) },
recovery::RecoveryError::SecretStorage(
matrix_sdk::encryption::secret_storage::SecretStorageError::ImportError { .. },
) => Self::Import { error_message: value.to_string() },
recovery::RecoveryError::SecretStorage(e) => {
Self::SecretStorage { error_message: e.to_string() }
}
@@ -287,6 +295,15 @@ impl Encryption {
Ok(self.inner.recovery().is_last_device().await?)
}
/// Does the user have other devices that the current device can verify
/// against?
///
/// The device must be signed by the user's cross-signing key, must have an
/// identity, and must not be a dehydrated device.
pub async fn has_devices_to_verify_against(&self) -> Result<bool, ClientError> {
Ok(self.inner.has_devices_to_verify_against().await?)
}
pub async fn wait_for_backup_upload_steady_state(
&self,
progress_listener: Option<Box<dyn BackupSteadyStateListener>>,
@@ -417,11 +434,13 @@ impl Encryption {
/// This method always tries to fetch the identity from the store, which we
/// only have if the user is tracked, meaning that we are both members
/// of the same encrypted room. If no user is found locally, a request will
/// be made to the homeserver.
/// be made to the homeserver unless `fallback_to_server` is set to `false`.
///
/// # Arguments
///
/// * `user_id` - The ID of the user that the identity belongs to.
/// * `fallback_to_server` - Should we request the user identity from the
/// homeserver if one isn't found locally.
///
/// Returns a `UserIdentity` if one is found. Returns an error if there
/// was an issue with the crypto store or with the request to the
@@ -431,6 +450,7 @@ impl Encryption {
pub async fn user_identity(
&self,
user_id: String,
fallback_to_server: bool,
) -> Result<Option<Arc<UserIdentity>>, ClientError> {
match self.inner.get_user_identity(user_id.as_str().try_into()?).await {
Ok(Some(identity)) => {
@@ -446,8 +466,12 @@ impl Encryption {
info!("Requesting identity from the server.");
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
if fallback_to_server {
let identity = self.inner.request_user_identity(user_id.as_str().try_into()?).await?;
Ok(identity.map(|identity| Arc::new(UserIdentity { inner: identity })))
} else {
Ok(None)
}
}
}
+18 -4
View File
@@ -5,14 +5,14 @@ use matrix_sdk::{
encryption::{identities::RequestVerificationError, CryptoStoreError},
event_cache::EventCacheError,
reqwest,
room::edit::EditError,
room::{calls::CallError, edit::EditError},
send_queue::RoomSendQueueError,
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
QueueWedgeError as SdkQueueWedgeError, StoreError,
};
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
use ruma::{
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter},
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
MilliSecondsSinceUnixEpoch,
};
use tracing::warn;
@@ -64,7 +64,9 @@ impl From<matrix_sdk::Error> for ClientError {
match e {
matrix_sdk::Error::Http(http_error) => {
if let Some(api_error) = http_error.as_client_api_error() {
if let ErrorBody::Standard { kind, message } = &api_error.body {
if let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
&api_error.body
{
let code = kind.errcode().to_string();
let Ok(kind) = kind.to_owned().try_into() else {
// We couldn't parse the API error, so we return a generic one instead
@@ -187,6 +189,12 @@ impl From<EditError> for ClientError {
}
}
impl From<CallError> for ClientError {
fn from(e: CallError) -> Self {
Self::from_err(e)
}
}
impl From<RoomSendQueueError> for ClientError {
fn from(e: RoomSendQueueError) -> Self {
Self::from_err(e)
@@ -211,6 +219,12 @@ impl From<RequestVerificationError> for ClientError {
}
}
impl From<spaces::Error> for ClientError {
fn from(e: spaces::Error) -> Self {
Self::from_err(e)
}
}
/// Bindings version of the sdk type replacing OwnedUserId/DeviceIds with simple
/// String.
///
+301 -24
View File
@@ -1,24 +1,24 @@
use std::ops::Deref;
use anyhow::{bail, Context};
use matrix_sdk::IdParseError;
use matrix_sdk_ui::timeline::TimelineEventItemId;
use ruma::{
events::{
room::{
encrypted,
message::{MessageType as RumaMessageType, Relation},
redaction::SyncRoomRedactionEvent,
},
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
TimelineEventType as RumaTimelineEventType,
},
EventId,
};
use crate::{
room_member::MembershipState,
ruma::{MessageType, NotifyType},
ruma::{MessageType, RtcNotificationType},
utils::Timestamp,
ClientError,
};
@@ -40,16 +40,30 @@ impl TimelineEvent {
self.0.origin_server_ts().into()
}
pub fn event_type(&self) -> Result<TimelineEventType, ClientError> {
let event_type = match self.0.deref() {
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
/// thread.
pub fn thread_root_event_id(&self) -> Option<String> {
match &*self.0 {
AnySyncTimelineEvent::MessageLike(event) => {
match event.original_content().and_then(|content| content.relation()) {
Some(encrypted::Relation::Thread(thread)) => Some(thread.event_id.to_string()),
_ => None,
}
}
AnySyncTimelineEvent::State(_) => None,
}
}
}
@@ -59,16 +73,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 },
}
@@ -153,7 +367,11 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
pub enum MessageLikeEventContent {
CallAnswer,
CallInvite,
CallNotify { notify_type: NotifyType },
RtcNotification {
notification_type: RtcNotificationType,
/// The timestamp at which this notification is considered invalid.
expiration_ts: Timestamp,
},
CallHangup,
CallCandidates,
KeyVerificationReady,
@@ -163,11 +381,21 @@ pub enum MessageLikeEventContent {
KeyVerificationKey,
KeyVerificationMac,
KeyVerificationDone,
Poll { question: String },
ReactionContent { related_event_id: String },
Poll {
question: String,
},
ReactionContent {
related_event_id: String,
},
RoomEncrypted,
RoomMessage { message_type: MessageType, in_reply_to_event_id: Option<String> },
RoomRedaction { redacted_event_id: Option<String>, reason: Option<String> },
RoomMessage {
message_type: MessageType,
in_reply_to_event_id: Option<String>,
},
RoomRedaction {
redacted_event_id: Option<String>,
reason: Option<String>,
},
Sticker,
}
@@ -178,10 +406,13 @@ impl TryFrom<AnySyncMessageLikeEvent> for MessageLikeEventContent {
let content = match value {
AnySyncMessageLikeEvent::CallAnswer(_) => MessageLikeEventContent::CallAnswer,
AnySyncMessageLikeEvent::CallInvite(_) => MessageLikeEventContent::CallInvite,
AnySyncMessageLikeEvent::CallNotify(content) => {
let original_content = get_message_like_event_original_content(content)?;
MessageLikeEventContent::CallNotify {
notify_type: original_content.notify_type.into(),
AnySyncMessageLikeEvent::RtcNotification(event) => {
let origin_server_ts = event.origin_server_ts();
let original_content = get_message_like_event_original_content(event)?;
let expiration_ts = original_content.expiration_ts(origin_server_ts, None).into();
MessageLikeEventContent::RtcNotification {
notification_type: original_content.notification_type.into(),
expiration_ts,
}
}
AnySyncMessageLikeEvent::CallHangup(_) => MessageLikeEventContent::CallHangup,
@@ -270,9 +501,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,
@@ -283,8 +516,10 @@ pub enum StateEventType {
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility,
RoomImagePack,
RoomJoinRules,
RoomMemberEvent,
RoomLanguage,
RoomName,
RoomPinnedEvents,
RoomPowerLevels,
@@ -294,12 +529,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,
@@ -310,7 +548,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,
@@ -321,17 +561,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,
CallNegotiate,
CallNotify,
CallReject,
CallSdpStreamMetadataChanged,
CallSelectAnswer,
Emote,
Encrypted,
File,
Image,
KeyVerificationAccept,
KeyVerificationCancel,
KeyVerificationDone,
@@ -339,6 +590,8 @@ pub enum MessageLikeEventType {
KeyVerificationMac,
KeyVerificationReady,
KeyVerificationStart,
Location,
Message,
PollEnd,
PollResponse,
PollStart,
@@ -346,20 +599,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::CallNotify => Self::CallNotify,
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,
@@ -367,17 +639,22 @@ 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,
MessageLikeEventType::Other(msgtype) => Self::from(msgtype),
}
}
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk::crypto::IdentityState;
use matrix_sdk_base::crypto::IdentityState;
#[derive(uniffi::Record)]
pub struct IdentityStatusChange {
+1
View File
@@ -26,6 +26,7 @@ mod ruma;
mod runtime;
mod session_verification;
mod spaces;
mod store;
mod sync_service;
mod task_handle;
mod timeline;
@@ -3,6 +3,7 @@ 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};
@@ -36,12 +37,16 @@ pub struct NotificationRoomInfo {
pub joined_members_count: u64,
pub is_encrypted: Option<bool>,
pub is_direct: bool,
pub is_space: bool,
}
#[derive(uniffi::Record)]
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,
@@ -51,6 +56,9 @@ pub struct NotificationItem {
pub is_noisy: Option<bool>,
pub has_mention: Option<bool>,
pub thread_id: Option<String>,
/// The push actions for this notification (notify, sound, highlight, etc.).
pub actions: Option<Vec<crate::notification_settings::Action>>,
}
impl NotificationItem {
@@ -63,8 +71,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,
@@ -79,10 +94,14 @@ impl NotificationItem {
joined_members_count: item.joined_members_count,
is_encrypted: item.is_room_encrypted,
is_direct: item.is_direct_message_room,
is_space: item.is_space,
},
is_noisy: item.is_noisy,
has_mention: item.has_mention,
thread_id: item.thread_id.map(|t| t.to_string()),
actions: item
.actions
.map(|a| a.into_iter().filter_map(|action| action.try_into().ok()).collect()),
}
}
}
@@ -11,6 +11,7 @@ use matrix_sdk::{
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use ruma::{
events::push_rules::PushRulesEventContent,
push::{
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
@@ -20,7 +21,7 @@ use ruma::{
};
use tokio::sync::RwLock as AsyncRwLock;
use crate::error::NotificationSettingsError;
use crate::error::{ClientError, NotificationSettingsError};
#[derive(Clone, Default, uniffi::Enum)]
pub enum ComparisonOperator {
@@ -167,6 +168,7 @@ impl TryFrom<SdkPushCondition> for PushCondition {
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
Ok(match value {
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
#[allow(deprecated)]
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
SdkPushCondition::RoomMemberCount { is } => {
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
@@ -189,6 +191,7 @@ impl From<PushCondition> for SdkPushCondition {
fn from(value: PushCondition) -> Self {
match value {
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
#[allow(deprecated)]
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
is: RoomMemberCountIs {
@@ -770,4 +773,11 @@ impl NotificationSettings {
.await?;
Ok(())
}
/// Returns the raw push rules in JSON format.
pub async fn get_raw_push_rules(&self) -> Result<Option<String>, ClientError> {
let raw_push_rules =
self.sdk_client.account().account_data::<PushRulesEventContent>().await?;
Ok(raw_push_rules.map(|raw| serde_json::to_string(&raw)).transpose()?)
}
}
+55 -14
View File
@@ -5,6 +5,8 @@ use std::sync::{atomic::AtomicBool, Arc};
#[cfg(feature = "sentry")]
use tracing::warn;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
#[cfg(feature = "sentry")]
use tracing_core::Level;
use tracing_core::Subscriber;
use tracing_subscriber::{
field::RecordFields,
@@ -21,6 +23,8 @@ use tracing_subscriber::{
EnvFilter, Layer, Registry,
};
#[cfg(feature = "sentry")]
use crate::tracing::BRIDGE_SPAN_NAME;
use crate::{error::ClientError, tracing::LogLevel};
// Adjusted version of tracing_subscriber::fmt::Format
@@ -274,7 +278,8 @@ enum LogTarget {
MatrixSdkBaseResponseProcessors,
// SDK common modules.
MatrixSdkCommonStoreLocks,
MatrixSdkCommonCrossProcessLock,
MatrixSdkCommonDeserializedResponses,
// SDK modules.
MatrixSdk,
@@ -284,6 +289,7 @@ enum LogTarget {
MatrixSdkEventCache,
MatrixSdkEventCacheStore,
MatrixSdkHttpClient,
MatrixSdkLatestEvents,
MatrixSdkOidc,
MatrixSdkSendQueue,
MatrixSdkSlidingSync,
@@ -302,7 +308,10 @@ impl LogTarget {
LogTarget::MatrixSdkBaseSlidingSync => "matrix_sdk_base::sliding_sync",
LogTarget::MatrixSdkBaseStoreAmbiguityMap => "matrix_sdk_base::store::ambiguity_map",
LogTarget::MatrixSdkBaseResponseProcessors => "matrix_sdk_base::response_processors",
LogTarget::MatrixSdkCommonStoreLocks => "matrix_sdk_common::store_locks",
LogTarget::MatrixSdkCommonCrossProcessLock => "matrix_sdk_common::cross_process_lock",
LogTarget::MatrixSdkCommonDeserializedResponses => {
"matrix_sdk_common::deserialized_responses"
}
LogTarget::MatrixSdk => "matrix_sdk",
LogTarget::MatrixSdkClient => "matrix_sdk::client",
LogTarget::MatrixSdkCrypto => "matrix_sdk_crypto",
@@ -311,6 +320,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",
@@ -333,20 +343,22 @@ 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::MatrixSdkCommonStoreLocks, LogLevel::Warn),
(LogTarget::MatrixSdkCommonCrossProcessLock, LogLevel::Warn),
(LogTarget::MatrixSdkCommonDeserializedResponses, LogLevel::Warn),
(LogTarget::MatrixSdkBaseStoreAmbiguityMap, LogLevel::Warn),
(LogTarget::MatrixSdkUiNotificationClient, LogLevel::Info),
(LogTarget::MatrixSdkBaseResponseProcessors, LogLevel::Debug),
];
const IMMUTABLE_LOG_TARGETS: &[LogTarget] = &[
LogTarget::Hyper, // Too verbose
LogTarget::MatrixSdk, // Too generic
LogTarget::MatrixSdkFfi, // Too verbose
LogTarget::MatrixSdkCommonStoreLocks, // Too verbose
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
LogTarget::Hyper, // Too verbose
LogTarget::MatrixSdk, // Too generic
LogTarget::MatrixSdkFfi, // Too verbose
LogTarget::MatrixSdkCommonCrossProcessLock, // Too verbose
LogTarget::MatrixSdkBaseStoreAmbiguityMap, // Too verbose
];
/// A log pack can be used to set the trace log level for a group of multiple
@@ -363,6 +375,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 {
@@ -374,15 +388,26 @@ impl TraceLogPacks {
LogTarget::MatrixSdkEventCache,
LogTarget::MatrixSdkBaseEventCache,
LogTarget::MatrixSdkEventCacheStore,
LogTarget::MatrixSdkCommonCrossProcessLock,
LogTarget::MatrixSdkCommonDeserializedResponses,
],
TraceLogPacks::SendQueue => &[LogTarget::MatrixSdkSendQueue],
TraceLogPacks::Timeline => &[LogTarget::MatrixSdkUiTimeline],
TraceLogPacks::Timeline => {
&[LogTarget::MatrixSdkUiTimeline, LogTarget::MatrixSdkCommonDeserializedResponses]
}
TraceLogPacks::NotificationClient => &[LogTarget::MatrixSdkUiNotificationClient],
TraceLogPacks::SyncProfiling => &[
LogTarget::MatrixSdkSlidingSync,
LogTarget::MatrixSdkBaseSlidingSync,
LogTarget::MatrixSdkBaseResponseProcessors,
LogTarget::MatrixSdkCrypto,
LogTarget::MatrixSdkCommonCrossProcessLock,
LogTarget::MatrixSdkCommonDeserializedResponses,
],
TraceLogPacks::LatestEvents => &[
LogTarget::MatrixSdkLatestEvents,
LogTarget::MatrixSdkSendQueue,
LogTarget::MatrixSdkEventCache,
],
}
}
@@ -454,7 +479,14 @@ impl TracingConfiguration {
let sentry_guard = sentry::init((
sentry_dsn,
sentry::ClientOptions {
traces_sample_rate: 0.0,
traces_sampler: Some(Arc::new(|ctx| {
// Make sure bridge spans are always uploaded
if ctx.name() == BRIDGE_SPAN_NAME {
1.0
} else {
0.0
}
})),
attach_stacktrace: true,
release: Some(env!("VERGEN_GIT_SHA").into()),
..sentry::ClientOptions::default()
@@ -488,7 +520,10 @@ impl TracingConfiguration {
move |metadata| {
if enabled.load(std::sync::atomic::Ordering::SeqCst) {
sentry_tracing::default_span_filter(metadata)
matches!(
metadata.level(),
&Level::ERROR | &Level::WARN | &Level::INFO | &Level::DEBUG
)
} else {
// Ignore, if sentry is globally disabled.
false
@@ -721,9 +756,11 @@ 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::store_locks=warn,
matrix_sdk_common::cross_process_lock=warn,
matrix_sdk_common::deserialized_responses=warn,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=info,
matrix_sdk_base::response_processors=debug,
@@ -765,9 +802,11 @@ 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::store_locks=warn,
matrix_sdk_common::cross_process_lock=warn,
matrix_sdk_common::deserialized_responses=trace,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=trace,
matrix_sdk_base::response_processors=trace,
@@ -810,9 +849,11 @@ 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::store_locks=warn,
matrix_sdk_common::cross_process_lock=warn,
matrix_sdk_common::deserialized_responses=trace,
matrix_sdk_base::store::ambiguity_map=warn,
matrix_sdk_ui::notification_client=info,
matrix_sdk_base::response_processors=debug,
+494 -17
View File
@@ -1,11 +1,222 @@
use std::sync::Arc;
use matrix_sdk::{
authentication::oauth::qrcode::{self, DeviceCodeErrorResponseType, LoginFailureReason},
crypto::types::qr_login::{LoginQrCodeDecodeError, QrCodeModeData},
use matrix_sdk::authentication::oauth::{
qrcode::{
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
},
OAuth,
};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use tracing::error;
use matrix_sdk_base::crypto::types::qr_login;
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
use crate::{
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
};
/// Handler for logging in with a QR code.
#[derive(uniffi::Object)]
pub struct LoginWithQrCodeHandler {
oauth: OAuth,
oidc_configuration: OidcConfiguration,
}
impl LoginWithQrCodeHandler {
pub(crate) fn new(oauth: OAuth, oidc_configuration: OidcConfiguration) -> Self {
Self { oauth, oidc_configuration }
}
}
#[matrix_sdk_ffi_macros::export]
impl LoginWithQrCodeHandler {
/// This method allows you to log in with a scanned QR code.
///
/// The existing device needs to display the QR code which this device can
/// scan, call this method and handle its progress updates to log in.
///
/// For the login to succeed, the [`Client`] associated with the
/// [`LoginWithQrCodeHandler`] must have been built with
/// [`QrCodeData::server_name`] as the server name.
///
/// This method uses the login mechanism described in [MSC4108]. As such,
/// it requires OAuth 2.0 support.
///
/// For the reverse flow where this device generates the QR code for the
/// existing device to scan, use [`LoginWithQrCodeHandler::generate`].
///
/// # Arguments
///
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
/// * `progress_listener` - A progress listener that must also be used to
/// transfer the [`CheckCode`] to the existing device.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn scan(
self: Arc<Self>,
qr_code_data: &QrCodeData,
progress_listener: Box<dyn QrLoginProgressListener>,
) -> Result<(), HumanQrLoginError> {
let registration_data = self
.oidc_configuration
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let login =
self.oauth.login_with_qr_code(Some(&registration_data)).scan(&qr_code_data.inner);
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
Ok(())
}
/// This method allows you to log in by generating a QR code.
///
/// This device needs to call this method and handle its progress updates to
/// generate a QR code which the existing device can scan and grant the
/// log in.
///
/// This method uses the login mechanism described in [MSC4108]. As such,
/// it requires OAuth 2.0 support.
///
/// For the reverse flow where the existing device generates the QR code
/// for this device to scan, use [`LoginWithQrCodeHandler::scan`].
///
/// # Arguments
///
/// * `progress_listener` - A progress listener that must also be used to
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn generate(
self: Arc<Self>,
progress_listener: Box<dyn GeneratedQrLoginProgressListener>,
) -> Result<(), HumanQrLoginError> {
let registration_data = self
.oidc_configuration
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;
let login = self.oauth.login_with_qr_code(Some(&registration_data)).generate();
let mut progress = login.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
login.await?;
Ok(())
}
}
/// Handler for granting login in with a QR code.
#[derive(uniffi::Object)]
pub struct GrantLoginWithQrCodeHandler {
oauth: OAuth,
}
impl GrantLoginWithQrCodeHandler {
pub(crate) fn new(oauth: OAuth) -> Self {
Self { oauth }
}
}
#[matrix_sdk_ffi_macros::export]
impl GrantLoginWithQrCodeHandler {
/// This method allows you to grant login with a scanned QR code.
///
/// The new device needs to display the QR code which this device can
/// scan, call this method and handle its progress updates to grant the
/// login.
///
/// This method uses the login mechanism described in [MSC4108]. As such,
/// it requires OAuth 2.0 support.
///
/// For the reverse flow where this device generates the QR code for the
/// existing device to scan, use [`GrantLoginWithQrCodeHandler::generate`].
///
/// # Arguments
///
/// * `qr_code_data` - The [`QrCodeData`] scanned from the QR code.
/// * `progress_listener` - A progress listener that must also be used to
/// transfer the [`CheckCode`] to the new device.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn scan(
self: Arc<Self>,
qr_code_data: &QrCodeData,
progress_listener: Box<dyn GrantQrLoginProgressListener>,
) -> Result<(), HumanQrGrantLoginError> {
let grant = self.oauth.grant_login_with_qr_code().scan(&qr_code_data.inner);
let mut progress = grant.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
grant.await?;
Ok(())
}
/// This method allows you to grant login by generating a QR code.
///
/// This device needs to call this method and handle its progress updates to
/// generate a QR code which the new device can scan to log in.
///
/// This method uses the login mechanism described in [MSC4108]. As such,
/// it requires OAuth 2.0 support.
///
/// For the reverse flow where the existing device generates the QR code
/// for this device to scan, use [`GrantLoginWithQrCodeHandler::scan`].
///
/// # Arguments
///
/// * `progress_listener` - A progress listener that must also be used to
/// obtain the [`QrCodeData`] and collect the [`CheckCode`] from the user.
///
/// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
pub async fn generate(
self: Arc<Self>,
progress_listener: Box<dyn GrantGeneratedQrLoginProgressListener>,
) -> Result<(), HumanQrGrantLoginError> {
let grant = self.oauth.grant_login_with_qr_code().generate();
let mut progress = grant.subscribe_to_progress();
// We create this task, which will get cancelled once it's dropped, just in case
// the progress stream doesn't end.
let _progress_task = TaskHandle::new(get_runtime_handle().spawn(async move {
while let Some(state) = progress.next().await {
progress_listener.on_update(state.into());
}
}));
grant.await?;
Ok(())
}
}
/// Data for the QR code login mechanism.
///
@@ -26,15 +237,26 @@ 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 {
QrCodeModeData::Reciprocate { server_name } => Some(server_name.to_owned()),
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,
}
}
}
@@ -46,7 +268,7 @@ pub enum QrCodeDecodeError {
#[error("Error decoding QR code: {error:?}")]
Crypto {
#[from]
error: LoginQrCodeDecodeError,
error: qrcode::LoginQrCodeDecodeError,
},
}
@@ -70,6 +292,12 @@ pub enum HumanQrLoginError {
OidcMetadataInvalid,
#[error("The other device is not signed in and as such can't sign in other devices.")]
OtherDeviceNotSignedIn,
#[error("The check code was already sent.")]
CheckCodeAlreadySent,
#[error("The check code could not be sent.")]
CheckCodeCannotBeSent,
#[error("The rendezvous session was not found and might have expired")]
NotFound,
}
impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
@@ -100,10 +328,14 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
| SecureChannelError::RendezvousChannel(_)
| SecureChannelError::UnsupportedQrCodeType => HumanQrLoginError::Unknown,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::InvalidCheckCode => HumanQrLoginError::ConnectionInsecure,
| SecureChannelError::InvalidCheckCode
| SecureChannelError::CannotReceiveCheckCode => {
HumanQrLoginError::ConnectionInsecure
}
SecureChannelError::InvalidIntent => HumanQrLoginError::OtherDeviceNotSignedIn,
},
@@ -112,12 +344,77 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
| QRCodeLoginError::DeviceKeyUpload(_)
| QRCodeLoginError::SessionTokens(_)
| QRCodeLoginError::UserIdDiscovery(_)
| QRCodeLoginError::SecretImport(_) => HumanQrLoginError::Unknown,
| QRCodeLoginError::SecretImport(_)
| QRCodeLoginError::ServerReset(_) => HumanQrLoginError::Unknown,
QRCodeLoginError::NotFound => HumanQrLoginError::NotFound,
}
}
}
/// Enum describing the progress of the QR-code login.
impl From<CheckCodeSenderError> for HumanQrLoginError {
fn from(value: CheckCodeSenderError) -> Self {
match value {
CheckCodeSenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent,
CheckCodeSenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent,
}
}
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
#[uniffi(flat_error)]
pub enum HumanQrGrantLoginError {
/// The requested device ID is already in use.
#[error("The requested device ID is already in use.")]
DeviceIDAlreadyInUse,
/// The check code was incorrect.
#[error("The check code was incorrect.")]
InvalidCheckCode,
/// The other client proposed an unsupported protocol.
#[error("Unsupported protocol: {0}")]
UnsupportedProtocol(String),
/// Secrets backup not set up properly.
#[error("Secrets backup not set up: {0}")]
MissingSecretsBackup(String),
/// The rendezvous session was not found and might have expired.
#[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),
}
impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
fn from(value: qrcode::QRCodeGrantLoginError) -> Self {
use qrcode::QRCodeGrantLoginError;
match value {
QRCodeGrantLoginError::DeviceIDAlreadyInUse => Self::DeviceIDAlreadyInUse,
QRCodeGrantLoginError::InvalidCheckCode => Self::InvalidCheckCode,
QRCodeGrantLoginError::UnableToCreateDevice => Self::UnableToCreateDevice,
QRCodeGrantLoginError::UnsupportedProtocol(protocol) => {
Self::UnsupportedProtocol(protocol.to_string())
}
QRCodeGrantLoginError::MissingSecretsBackup(error) => {
Self::MissingSecretsBackup(error.map_or("other".to_owned(), |e| e.to_string()))
}
QRCodeGrantLoginError::NotFound => Self::NotFound,
QRCodeGrantLoginError::Unknown(string) => Self::Unknown(string),
}
}
}
/// Enum describing the progress of logging in by scanning a QR code that was
/// generated on an existing device.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum QrLoginProgress {
/// The login process is starting.
@@ -136,6 +433,8 @@ pub enum QrLoginProgress {
/// We are waiting for the login and for the OAuth 2.0 authorization server
/// to give us an access token.
WaitingForToken { user_code: String },
/// We are syncing secrets.
SyncingSecrets,
/// The login has successfully finished.
Done,
}
@@ -145,13 +444,13 @@ pub trait QrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: QrLoginProgress);
}
impl From<qrcode::LoginProgress> for QrLoginProgress {
fn from(value: qrcode::LoginProgress) -> Self {
impl From<qrcode::LoginProgress<QrProgress>> for QrLoginProgress {
fn from(value: qrcode::LoginProgress<QrProgress>) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel { check_code } => {
LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
@@ -160,7 +459,185 @@ impl From<qrcode::LoginProgress> for QrLoginProgress {
}
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
LoginProgress::Done => Self::Done,
}
}
}
/// Enum describing the progress of logging in by generating a QR code and
/// having an existing device scan it.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum GeneratedQrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We have established the secure channel and now need to display the
/// QR code so that the existing device can scan it.
QrReady { qr_code: Arc<QrCodeData> },
/// The existing device has scanned the QR code and is displaying the
/// checkcode. We now need to ask the user to enter the checkcode so that
/// we can verify that the channel is indeed secure.
QrScanned { check_code_sender: Arc<CheckCodeSender> },
/// We are waiting for the login and for the OAuth 2.0 authorization server
/// to give us an access token.
WaitingForToken { user_code: String },
/// We are syncing secrets.
SyncingSecrets,
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait GeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: GeneratedQrLoginProgress);
}
impl From<qrcode::LoginProgress<GeneratedQrProgress>> for GeneratedQrLoginProgress {
fn from(value: qrcode::LoginProgress<GeneratedQrProgress>) -> Self {
use qrcode::LoginProgress;
match value {
LoginProgress::Starting => Self::Starting,
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
}
LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(inner)) => {
Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) }
}
LoginProgress::WaitingForToken { user_code } => Self::WaitingForToken { user_code },
LoginProgress::SyncingSecrets => Self::SyncingSecrets,
LoginProgress::Done => Self::Done,
}
}
}
/// Enum describing the progress of granting login in by scanning a QR code that
/// was generated on a new device.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum GrantQrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We established a secure channel with the other device.
EstablishingSecureChannel {
/// The check code that the device should display so the other device
/// can confirm that the channel is secure as well.
check_code: u8,
/// The string representation of the check code, will be guaranteed to
/// be 2 characters long, preserving the leading zero if the
/// first digit is a zero.
check_code_string: String,
},
/// The secure channel has been confirmed using the [`CheckCode`] and this
/// device is waiting for the authorization to complete.
WaitingForAuth {
/// A URI to open in a (secure) system browser to verify the new login.
verification_uri: String,
},
/// We are syncing secrets.
SyncingSecrets,
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait GrantQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: GrantQrLoginProgress);
}
impl From<qrcode::GrantLoginProgress<QrProgress>> for GrantQrLoginProgress {
fn from(value: qrcode::GrantLoginProgress<QrProgress>) -> Self {
use qrcode::GrantLoginProgress;
match value {
GrantLoginProgress::Starting => Self::Starting,
GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => {
let check_code = check_code.to_digit();
Self::EstablishingSecureChannel {
check_code,
check_code_string: format!("{check_code:02}"),
}
}
GrantLoginProgress::WaitingForAuth { verification_uri } => {
Self::WaitingForAuth { verification_uri: verification_uri.into() }
}
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
GrantLoginProgress::Done => Self::Done,
}
}
}
/// Enum describing the progress of granting login by generating a QR code to
/// be scanned on the new device.
#[derive(Debug, Default, Clone, uniffi::Enum)]
pub enum GrantGeneratedQrLoginProgress {
/// The login process is starting.
#[default]
Starting,
/// We have established the secure channel and now need to display the
/// QR code so that the existing device can scan it.
QrReady { qr_code: Arc<QrCodeData> },
/// The existing device has scanned the QR code and is displaying the
/// checkcode. We now need to ask the user to enter the checkcode so that
/// we can verify that the channel is indeed secure.
QrScanned { check_code_sender: Arc<CheckCodeSender> },
/// The secure channel has been confirmed using the [`CheckCode`] and this
/// device is waiting for the authorization to complete.
WaitingForAuth {
/// A URI to open in a (secure) system browser to verify the new login.
verification_uri: String,
},
/// We are syncing secrets.
SyncingSecrets,
/// The login has successfully finished.
Done,
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait GrantGeneratedQrLoginProgressListener: SyncOutsideWasm + SendOutsideWasm {
fn on_update(&self, state: GrantGeneratedQrLoginProgress);
}
impl From<qrcode::GrantLoginProgress<GeneratedQrProgress>> for GrantGeneratedQrLoginProgress {
fn from(value: qrcode::GrantLoginProgress<GeneratedQrProgress>) -> Self {
use qrcode::GrantLoginProgress;
match value {
GrantLoginProgress::Starting => Self::Starting,
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady(inner)) => {
Self::QrReady { qr_code: Arc::new(QrCodeData { inner }) }
}
GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned(
inner,
)) => Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) },
GrantLoginProgress::WaitingForAuth { verification_uri } => {
Self::WaitingForAuth { verification_uri: verification_uri.into() }
}
GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets,
GrantLoginProgress::Done => Self::Done,
}
}
}
#[derive(Debug, uniffi::Object)]
/// Used to pass back the [`CheckCode`] entered by the user to verify that the
/// secure channel is indeed secure.
pub struct CheckCodeSender {
inner: SdkCheckCodeSender,
}
#[matrix_sdk_ffi_macros::export]
impl CheckCodeSender {
/// Send the [`CheckCode`].
///
/// Calling this method more than once will result in an error.
///
/// # Arguments
///
/// * `check_code` - The check code in digits representation.
pub async fn send(&self, code: u8) -> Result<(), HumanQrLoginError> {
self.inner.send(code).await.map_err(HumanQrLoginError::from)
}
}
+595 -41
View File
@@ -1,14 +1,17 @@
use std::{collections::HashMap, pin::pin, sync::Arc};
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
use anyhow::{Context, Result};
use futures_util::{pin_mut, StreamExt};
use matrix_sdk::{
crypto::LocalTrust,
encryption::LocalTrust,
room::{
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
edit::EditedContent, power_levels::RoomPowerLevelChanges,
ListThreadsOptions as SdkListThreadsOptions, Room as SdkRoom, RoomMemberRole,
TryFromReportedContentScoreError,
},
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType, EncryptionState,
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
SuccessorRoom as SdkSuccessorRoom,
};
@@ -19,14 +22,15 @@ use matrix_sdk_ui::{
};
use mime::Mime;
use ruma::{
api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads,
assign,
events::{
call::notify,
receipt::ReceiptThread,
room::{
avatar::ImageInfo as RumaAvatarImageInfo,
history_visibility::HistoryVisibility as RumaHistoryVisibility,
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
MediaSource,
MediaSource as RumaMediaSource,
},
AnyMessageLikeEventContent, AnySyncTimelineEvent,
},
@@ -39,16 +43,19 @@ use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
use crate::{
chunk_iterator::ChunkIterator,
client::{JoinRule, RoomVisibility},
error::{ClientError, MediaInfoError, NotYetImplemented, RoomError},
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
event::TimelineEvent,
identity_status_change::IdentityStatusChange,
live_location_share::{LastLocation, LiveLocationShare},
room_member::{RoomMember, RoomMemberWithSenderInfo},
room_preview::RoomPreview,
ruma::{ImageInfo, LocationContent},
ruma::{
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
},
runtime::get_runtime_handle,
timeline::{
configuration::{TimelineConfiguration, TimelineFilter},
EventTimelineItem, LatestEventValue, ReceiptType, SendHandle, Timeline,
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
},
utils::{u64_to_uint, AsyncRuntimeDropped},
TaskHandle,
@@ -227,11 +234,8 @@ impl Room {
builder = builder
.with_focus(configuration.focus.try_into()?)
.with_date_divider_mode(configuration.date_divider_mode.into());
if configuration.track_read_receipts {
builder = builder.track_read_marker_and_receipts();
}
.with_date_divider_mode(configuration.date_divider_mode.into())
.track_read_marker_and_receipts(configuration.track_read_receipts);
match configuration.filter {
TimelineFilter::All => {
@@ -255,10 +259,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)
});
}
}
@@ -300,12 +304,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> {
@@ -345,6 +345,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:
@@ -670,6 +678,25 @@ impl Room {
Ok(())
}
/// Mark a room as fully read, by attaching a read receipt to the provided
/// `event_id`.
///
/// **Warning:** using this method is **NOT** recommended, as providing the
/// latest event id can cause incorrect read receipts. This method won't
/// check if sending the read receipt is necessary or valid. It should
/// *only* be used when some constraint prevents you from instantiating a
/// [`Timeline`]. For any other case use [`Timeline::mark_as_read`]
/// instead.
pub async fn mark_as_fully_read_unchecked(&self, event_id: String) -> Result<(), ClientError> {
let event_id = EventId::parse(event_id)?;
self.inner
.send_single_receipt(ReceiptType::FullyRead.into(), ReceiptThread::Unthreaded, event_id)
.await?;
Ok(())
}
pub async fn get_power_levels(&self) -> Result<Arc<RoomPowerLevels>, ClientError> {
let power_levels = self.inner.power_levels().await.map_err(matrix_sdk::Error::from)?;
Ok(Arc::new(RoomPowerLevels::new(power_levels, self.inner.own_user_id().to_owned())))
@@ -735,6 +762,37 @@ impl Room {
self.inner.send_queue().set_enabled(enable);
}
/// Subscribe to all send queue updates in this room.
///
/// The given listener will be immediately called with
/// `RoomSendQueueUpdate::NewLocalEvent` for each local echo existing in
/// the queue.
pub async fn subscribe_to_send_queue_updates(
&self,
listener: Box<dyn SendQueueListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let q = self.inner.send_queue();
let (local_echoes, mut subscriber) = q.subscribe().await?;
for local_echo in local_echoes {
listener.on_update(RoomSendQueueUpdate::NewLocalEvent {
transaction_id: local_echo.transaction_id.into(),
});
}
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
loop {
match subscriber.recv().await {
Ok(update) => match update.try_into() {
Ok(update) => listener.on_update(update),
Err(err) => error!("error when converting send queue update: {err}"),
},
Err(err) => error!("error when listening for send queue updates: {err}"),
}
}
}))))
}
/// Store the given `ComposerDraft` in the state store using the current
/// room id, as identifier.
pub async fn save_composer_draft(
@@ -1010,6 +1068,44 @@ impl Room {
Ok(())
}
/// Declines a call (and stop ringing).
///
/// # Arguments
///
/// * `rtc_notification_event_id` - the event id of the m.rtc.notification
/// event.
pub async fn decline_call(&self, rtc_notification_event_id: String) -> Result<(), ClientError> {
let parsed_id = EventId::parse(rtc_notification_event_id.as_str())?;
let content = self.inner.make_decline_call_event(&parsed_id).await?;
self.inner.send_queue().send(content.into()).await?;
Ok(())
}
/// Subscribes to call decline for a currently ringing call, using a
/// `listener` to be notified when someone declines.
///
/// Will error if `rtc_notification_event_id` is not a valid event id.
/// Use the [`TaskHandle`] to cancel the subscription.
pub fn subscribe_to_call_decline_events(
self: Arc<Self>,
rtc_notification_event_id: String,
listener: Box<dyn CallDeclineListener>,
) -> Result<Arc<TaskHandle>, ClientError> {
let parsed_id = EventId::parse(rtc_notification_event_id.as_str())?;
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
let (_event_handler_drop_guard, mut subscriber) =
self.inner.subscribe_to_call_decline_events(&parsed_id);
while let Ok(user_id) = subscriber.recv().await {
listener.call(user_id.to_string());
}
}))))
}
/// Subscribes to live location shares in this room, using a `listener` to
/// be notified of the changes.
///
@@ -1141,6 +1237,57 @@ impl Room {
.await?
.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
/// [`ThreadRoots::prev_batch_token`]. This token can be passed to the next
/// call to this function, through the `from` field of
/// [`ListThreadsOptions`].
pub async fn list_threads(&self, opts: ListThreadsOptions) -> Result<ThreadRoots, ClientError> {
let inner_opts = SdkListThreadsOptions {
include_threads: match opts.include_threads {
IncludeThreads::All => SdkIncludeThreads::All,
IncludeThreads::Participated => SdkIncludeThreads::Participated,
},
from: opts.from,
limit: opts.limit.and_then(ruma::UInt::new),
};
let roots = self.inner.list_threads(inner_opts).await?;
Ok(ThreadRoots {
chunk: roots
.chunk
.into_iter()
.filter_map(|timeline_event| {
timeline_event
.raw()
.deserialize()
.ok()
.map(|any_timeline_event| TimelineEvent(Box::new(any_timeline_event)))
})
.collect(),
prev_batch_token: roots.prev_batch_token,
})
}
/// 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(
&self,
event_id: String,
) -> Result<TimelineEvent, ClientError> {
let event_id = EventId::parse(event_id)?;
let timeline_event = self.inner.load_or_fetch_event(&event_id, None).await?;
Ok(timeline_event
.kind
.into_raw()
.deserialize()?
.into_full_event(self.inner.room_id().to_owned())
.into())
}
}
/// A thread subscription (MSC4306).
@@ -1151,12 +1298,72 @@ pub struct ThreadSubscription {
automatic: bool,
}
/// Options for [Room::list_threads].
#[derive(Debug, Clone, uniffi::Record)]
pub struct ListThreadsOptions {
/// An extra filter to select which threads should be returned.
pub include_threads: IncludeThreads,
/// The token to start returning events from.
///
/// This token can be obtained from a [`ThreadRoots::prev_batch_token`]
/// returned by a previous call to [`Room::list_threads()`].
///
/// If `from` isn't provided the homeserver shall return a list of thread
/// roots from end of the timeline history.
pub from: Option<String>,
/// The maximum number of events to return.
///
/// Default: 10.
pub limit: Option<u64>,
}
/// Which threads to include in the response.
#[derive(Debug, Clone, uniffi::Enum)]
pub enum IncludeThreads {
/// `all`
///
/// Include all thread roots found in the room.
///
/// This is the default.
All,
/// `participated`
///
/// Only include thread roots for threads where
/// [`current_user_participated`] is `true`.
///
/// [`current_user_participated`]: https://spec.matrix.org/latest/client-server-api/#server-side-aggregation-of-mthread-relationships
Participated,
}
/// The result of a [`Room::list_threads`] query.
///
/// This is a wrapper around the Ruma equivalent, with events decrypted if needs
/// be.
#[derive(uniffi::Object)]
pub struct ThreadRoots {
/// The events that are thread roots in the current batch.
pub chunk: Vec<TimelineEvent>,
/// Token to paginate backwards in a subsequent query to
/// [`Room::list_threads`].
pub prev_batch_token: Option<String>,
}
/// A listener for receiving new live location shares in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
}
/// A listener for receiving call decline events in a room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait CallDeclineListener: SyncOutsideWasm + SendOutsideWasm {
fn call(&self, decliner_user_id: String);
}
impl From<matrix_sdk::room::knock_requests::KnockRequest> for KnockRequest {
fn from(request: matrix_sdk::room::knock_requests::KnockRequest) -> Self {
Self {
@@ -1321,8 +1528,8 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
fn try_from(value: ImageInfo) -> Result<Self, MediaInfoError> {
let thumbnail_url = if let Some(media_source) = value.thumbnail_source {
match &media_source.as_ref().media_source {
MediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
MediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
RumaMediaSource::Plain(mxc_uri) => Some(mxc_uri.clone()),
RumaMediaSource::Encrypted(_) => return Err(MediaInfoError::InvalidField),
}
} else {
None
@@ -1340,18 +1547,6 @@ impl TryFrom<ImageInfo> for RumaAvatarImageInfo {
}
}
#[derive(uniffi::Enum)]
pub enum RtcApplicationType {
Call,
}
impl From<RtcApplicationType> for notify::ApplicationType {
fn from(value: RtcApplicationType) -> Self {
match value {
RtcApplicationType::Call => notify::ApplicationType::Call,
}
}
}
/// Current draft of the composer for the room.
#[derive(uniffi::Record)]
pub struct ComposerDraft {
@@ -1362,21 +1557,257 @@ pub struct ComposerDraft {
pub html_text: Option<String>,
/// The type of draft.
pub draft_type: ComposerDraftType,
/// Attachments associated with this draft.
pub attachments: Vec<DraftAttachment>,
}
impl From<SdkComposerDraft> for ComposerDraft {
fn from(value: SdkComposerDraft) -> Self {
let SdkComposerDraft { plain_text, html_text, draft_type } = value;
Self { plain_text, html_text, draft_type: draft_type.into() }
let SdkComposerDraft { plain_text, html_text, draft_type, attachments } = value;
Self {
plain_text,
html_text,
draft_type: draft_type.into(),
attachments: attachments.into_iter().map(|a| a.into()).collect(),
}
}
}
impl TryFrom<ComposerDraft> for SdkComposerDraft {
type Error = ruma::IdParseError;
type Error = ClientError;
fn try_from(value: ComposerDraft) -> std::result::Result<Self, Self::Error> {
let ComposerDraft { plain_text, html_text, draft_type } = value;
Ok(Self { plain_text, html_text, draft_type: draft_type.try_into()? })
let ComposerDraft { plain_text, html_text, draft_type, attachments } = value;
Ok(Self {
plain_text,
html_text,
draft_type: draft_type.try_into()?,
attachments: attachments
.into_iter()
.map(|a| a.try_into())
.collect::<std::result::Result<Vec<_>, _>>()?,
})
}
}
/// An attachment stored with a composer draft.
#[derive(uniffi::Enum)]
pub enum DraftAttachment {
Audio { audio_info: AudioInfo, source: UploadSource },
File { file_info: FileInfo, source: UploadSource },
Image { image_info: ImageInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
Video { video_info: VideoInfo, source: UploadSource, thumbnail_source: Option<UploadSource> },
}
impl From<SdkDraftAttachment> for DraftAttachment {
fn from(value: SdkDraftAttachment) -> Self {
match value.content {
DraftAttachmentContent::Image {
data,
mimetype,
size,
width,
height,
blurhash,
thumbnail,
} => {
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
bytes: t.data.clone(),
filename: t.filename.clone(),
});
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
width: t.width,
height: t.height,
mimetype: t.mimetype,
size: t.size,
});
DraftAttachment::Image {
image_info: ImageInfo {
height,
width,
mimetype,
size,
thumbnail_info,
thumbnail_source: None,
blurhash,
is_animated: None,
},
source: UploadSource::Data { bytes: data, filename: value.filename },
thumbnail_source,
}
}
DraftAttachmentContent::Video {
data,
mimetype,
size,
width,
height,
duration,
blurhash,
thumbnail,
} => {
let thumbnail_source = thumbnail.as_ref().map(|t| UploadSource::Data {
bytes: t.data.clone(),
filename: t.filename.clone(),
});
let thumbnail_info = thumbnail.map(|t| ThumbnailInfo {
width: t.width,
height: t.height,
mimetype: t.mimetype,
size: t.size,
});
DraftAttachment::Video {
video_info: VideoInfo {
duration,
height,
width,
mimetype,
size,
thumbnail_info,
thumbnail_source: None,
blurhash,
},
source: UploadSource::Data { bytes: data, filename: value.filename },
thumbnail_source,
}
}
DraftAttachmentContent::Audio { data, mimetype, size, duration } => {
DraftAttachment::Audio {
audio_info: AudioInfo { duration, size, mimetype },
source: UploadSource::Data { bytes: data, filename: value.filename },
}
}
DraftAttachmentContent::File { data, mimetype, size } => DraftAttachment::File {
file_info: FileInfo {
mimetype,
size,
thumbnail_info: None,
thumbnail_source: None,
},
source: UploadSource::Data { bytes: data, filename: value.filename },
},
}
}
}
/// Resolve the bytes and filename from an `UploadSource`, reading the file
/// contents if needed.
fn read_upload_source(source: UploadSource) -> Result<(Vec<u8>, String), ClientError> {
match source {
UploadSource::Data { bytes, filename } => Ok((bytes, filename)),
UploadSource::File { filename } => {
let path: PathBuf = filename.into();
let filename = path
.file_name()
.ok_or(ClientError::Generic {
msg: "Invalid attachment path".to_owned(),
details: None,
})?
.to_str()
.ok_or(ClientError::Generic {
msg: "Invalid attachment path".to_owned(),
details: None,
})?
.to_owned();
let bytes = fs::read(&path).map_err(|_| ClientError::Generic {
msg: "Could not load file".to_owned(),
details: None,
})?;
Ok((bytes, filename))
}
}
}
impl TryFrom<DraftAttachment> for SdkDraftAttachment {
type Error = ClientError;
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
match value {
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
let (data, filename) = read_upload_source(source)?;
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let (data, filename) = read_upload_source(source)?;
Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
})
}
_ => None,
};
Ok(Self {
filename,
content: DraftAttachmentContent::Image {
data,
mimetype: image_info.mimetype,
size: image_info.size,
width: image_info.width,
height: image_info.height,
blurhash: image_info.blurhash,
thumbnail,
},
})
}
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
let (data, filename) = read_upload_source(source)?;
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
(Some(info), Some(source)) => {
let (data, filename) = read_upload_source(source)?;
Some(DraftThumbnail {
filename,
data,
mimetype: info.mimetype,
width: info.width,
height: info.height,
size: info.size,
})
}
_ => None,
};
Ok(Self {
filename,
content: DraftAttachmentContent::Video {
data,
mimetype: video_info.mimetype,
size: video_info.size,
width: video_info.width,
height: video_info.height,
duration: video_info.duration,
blurhash: video_info.blurhash,
thumbnail,
},
})
}
DraftAttachment::Audio { audio_info, source, .. } => {
let (data, filename) = read_upload_source(source)?;
Ok(Self {
filename,
content: DraftAttachmentContent::Audio {
data,
mimetype: audio_info.mimetype,
size: audio_info.size,
duration: audio_info.duration,
},
})
}
DraftAttachment::File { file_info, source, .. } => {
let (data, filename) = read_upload_source(source)?;
Ok(Self {
filename,
content: DraftAttachmentContent::File {
data,
mimetype: file_info.mimetype,
size: file_info.size,
},
})
}
}
}
}
@@ -1521,3 +1952,126 @@ impl From<SdkPredecessorRoom> for PredecessorRoom {
Self { room_id: value.room_id.to_string() }
}
}
/// A listener to send queue updates in a specific room.
#[matrix_sdk_ffi_macros::export(callback_interface)]
pub trait SendQueueListener: SyncOutsideWasm + SendOutsideWasm {
/// Called every time the send queue dispatches an update for the given
/// room.
fn on_update(&self, update: RoomSendQueueUpdate);
}
/// An update to a room send queue.
#[derive(uniffi::Enum)]
pub enum RoomSendQueueUpdate {
/// A new local event is being sent.
NewLocalEvent {
/// Transaction id used to identify this event.
transaction_id: String,
},
/// A local event that hadn't been sent to the server yet has been cancelled
/// before sending.
CancelledLocalEvent {
/// Transaction id used to identify this event.
transaction_id: String,
},
/// A local event's content has been replaced with something else.
ReplacedLocalEvent {
/// Transaction id used to identify this event.
transaction_id: String,
},
/// An error happened when an event was being sent.
///
/// The event has not been removed from the queue. All the send queues
/// will be disabled after this happens, and must be manually re-enabled.
SendError {
/// Transaction id used to identify this event.
transaction_id: String,
/// Error received while sending the event.
error: QueueWedgeError,
/// Whether the error is considered recoverable or not.
///
/// An error that's recoverable will disable the room's send queue,
/// while an unrecoverable error will be parked, until the user
/// decides to cancel sending it.
is_recoverable: bool,
},
/// The event has been unwedged and sending is now being retried.
RetryEvent {
/// Transaction id used to identify this event.
transaction_id: String,
},
/// The event has been sent to the server, and the query returned
/// successfully.
SentEvent {
/// Transaction id used to identify this event.
transaction_id: String,
/// Received event id from the send response.
event_id: String,
},
/// A media upload (consisting of a file and possibly a thumbnail) has made
/// progress.
MediaUpload {
/// The media event this uploaded media relates to.
related_to: String,
/// The final media source for the file if it has finished uploading.
file: Option<Arc<MediaSource>>,
/// The index of the media within the transaction. A file and its
/// thumbnail share the same index. Will always be 0 for non-gallery
/// media uploads.
index: u64,
/// The combined upload progress across the file and, if existing, its
/// thumbnail. For gallery uploads, the progress is reported per indexed
/// gallery item.
progress: AbstractProgress,
},
}
impl TryFrom<SdkRoomSendQueueUpdate> for RoomSendQueueUpdate {
type Error = ClientError;
fn try_from(value: SdkRoomSendQueueUpdate) -> std::result::Result<Self, Self::Error> {
Ok(match value {
SdkRoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
Self::CancelledLocalEvent { transaction_id: transaction_id.into() }
}
SdkRoomSendQueueUpdate::MediaUpload { related_to, file, index, progress } => {
Self::MediaUpload {
related_to: related_to.into(),
file: file.map(|source| source.try_into().map(Arc::new)).transpose()?,
index,
progress: progress.into(),
}
}
SdkRoomSendQueueUpdate::NewLocalEvent(local_echo) => {
Self::NewLocalEvent { transaction_id: local_echo.transaction_id.into() }
}
SdkRoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, .. } => {
Self::ReplacedLocalEvent { transaction_id: transaction_id.into() }
}
SdkRoomSendQueueUpdate::RetryEvent { transaction_id } => {
Self::RetryEvent { transaction_id: transaction_id.into() }
}
SdkRoomSendQueueUpdate::SendError { transaction_id, error, is_recoverable } => {
let as_queue_wedge_error: matrix_sdk::QueueWedgeError = (&*error).into();
Self::SendError {
transaction_id: transaction_id.into(),
error: as_queue_wedge_error.into(),
is_recoverable,
}
}
SdkRoomSendQueueUpdate::SentEvent { transaction_id, event_id } => {
Self::SentEvent { transaction_id: transaction_id.into(), event_id: event_id.into() }
}
})
}
}
@@ -29,6 +29,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> {
@@ -206,6 +210,8 @@ pub struct RoomPowerLevelsValues {
pub room_avatar: i64,
/// The level required to change the room's topic.
pub room_topic: i64,
/// The level required to change the space's children.
pub space_child: i64,
}
impl From<RumaPowerLevels> for RoomPowerLevelsValues {
@@ -228,6 +234,7 @@ impl From<RumaPowerLevels> for RoomPowerLevelsValues {
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
space_child: state_event_level_for(&value, &TimelineEventType::SpaceChild),
}
}
}
+32 -7
View File
@@ -15,10 +15,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,6 +168,15 @@ 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;
@@ -216,7 +225,10 @@ 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(page_size.try_into().unwrap());
this.inner.entries_with_dynamic_adapters_with(
page_size.try_into().unwrap(),
enable_latest_event_sorter,
);
// FFI dance to make those values consumable by foreign language, nothing fancy
// here, that's the real code for this method.
@@ -231,7 +243,12 @@ impl RoomList {
listener.on_update(
diffs
.into_iter()
.map(|room| RoomListEntriesUpdate::from(utd_hook.clone(), room))
.map(|diff| {
RoomListEntriesUpdate::from(
utd_hook.clone(),
diff.map(|room| room.into_inner()),
)
})
.collect(),
);
}
@@ -455,7 +472,9 @@ impl RoomListDynamicEntriesController {
pub enum RoomListEntriesDynamicFilterKind {
All { filters: Vec<RoomListEntriesDynamicFilterKind> },
Any { filters: Vec<RoomListEntriesDynamicFilterKind> },
Identifiers { identifiers: Vec<String> },
NonSpace,
Space,
NonLeft,
// Not { filter: RoomListEntriesDynamicFilterKind } - requires recursive enum
// support in uniffi https://github.com/mozilla/uniffi-rs/issues/396
@@ -464,6 +483,7 @@ pub enum RoomListEntriesDynamicFilterKind {
Favourite,
LowPriority,
NonLowPriority,
NonFavorite,
Invite,
Category { expect: RoomListFilterCategory },
None,
@@ -498,13 +518,18 @@ impl From<RoomListEntriesDynamicFilterKind> for BoxedFilterFn {
Kind::Any { filters } => Box::new(new_filter_any(
filters.into_iter().map(|filter| BoxedFilterFn::from(filter)).collect(),
)),
Kind::NonLeft => Box::new(new_filter_non_left()),
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()),
Kind::Joined => Box::new(new_filter_joined()),
Kind::Unread => Box::new(new_filter_unread()),
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()),
@@ -93,7 +93,6 @@ pub struct RoomMember {
pub membership: MembershipState,
pub is_name_ambiguous: bool,
pub power_level: PowerLevel,
pub normalized_power_level: PowerLevel,
pub is_ignored: bool,
pub suggested_role_for_power_level: RoomMemberRole,
pub membership_change_reason: Option<String>,
@@ -110,7 +109,6 @@ impl TryFrom<SdkRoomMember> for RoomMember {
membership: m.membership().clone().try_into()?,
is_name_ambiguous: m.name_ambiguous(),
power_level: m.power_level().try_into()?,
normalized_power_level: m.normalized_power_level().try_into()?,
is_ignored: m.is_ignored(),
suggested_role_for_power_level: m.suggested_role_for_power_level(),
membership_change_reason: m.event().reason().map(|s| s.to_owned()),
+12 -12
View File
@@ -23,7 +23,6 @@ use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVid
use ruma::{
assign,
events::{
call::notify::NotifyType as RumaNotifyType,
direct::DirectEventContent,
fully_read::FullyReadEventContent,
identity_server::IdentityServerEventContent,
@@ -57,6 +56,7 @@ use ruma::{
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
ThumbnailInfo as RumaThumbnailInfo,
},
rtc::notification::NotificationType as RumaNotificationType,
secret_storage::{
default_key::SecretStorageDefaultKeyEventContent,
key::{
@@ -487,25 +487,25 @@ impl TryFrom<RumaMessageType> for MessageType {
}
#[derive(Clone, uniffi::Enum)]
pub enum NotifyType {
pub enum RtcNotificationType {
Ring,
Notify,
Notification,
}
impl From<RumaNotifyType> for NotifyType {
fn from(val: RumaNotifyType) -> Self {
impl From<RumaNotificationType> for RtcNotificationType {
fn from(val: RumaNotificationType) -> Self {
match val {
RumaNotifyType::Ring => Self::Ring,
_ => Self::Notify,
RumaNotificationType::Ring => Self::Ring,
_ => Self::Notification,
}
}
}
impl From<NotifyType> for RumaNotifyType {
fn from(value: NotifyType) -> Self {
impl From<RtcNotificationType> for RumaNotificationType {
fn from(value: RtcNotificationType) -> Self {
match value {
NotifyType::Ring => RumaNotifyType::Ring,
NotifyType::Notify => RumaNotifyType::Notify,
RtcNotificationType::Ring => RumaNotificationType::Ring,
RtcNotificationType::Notification => RumaNotificationType::Notification,
}
}
}
@@ -736,7 +736,7 @@ impl TryFrom<&AudioInfo> for BaseAudioInfo {
let size = UInt::try_from(value.size.ok_or(MediaInfoError::MissingField)?)
.map_err(|_| MediaInfoError::InvalidField)?;
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size) })
Ok(BaseAudioInfo { duration: Some(duration), size: Some(size), waveform: None })
}
}
+290 -16
View File
@@ -18,8 +18,10 @@ use eyeball_im::VectorDiff;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::spaces::{
room_list::SpaceRoomListPaginationState, SpaceRoom as UISpaceRoom,
SpaceRoomList as UISpaceRoomList, SpaceService as UISpaceService,
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
room_list::SpaceRoomListPaginationState,
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
SpaceService as UISpaceService,
};
use ruma::RoomId;
@@ -54,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(),
@@ -77,22 +79,114 @@ 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::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()
}
/// Returns a `SpaceRoomList` for the given space ID.
#[allow(clippy::unused_async)]
// This method doesn't need to be async but if its not the FFI layer panics
// with "there is no no reactor running, must be called from the context
// of a Tokio 1.x runtime" error because the underlying constructor spawns
// an async task.
pub async fn space_room_list(
&self,
space_id: String,
) -> Result<Arc<SpaceRoomList>, ClientError> {
let space_id = RoomId::parse(space_id)?;
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id))))
Ok(Arc::new(SpaceRoomList::new(self.inner.space_room_list(space_id).await)))
}
/// Returns all known direct-parents of a given space room ID.
pub async fn joined_parents_of_child(
&self,
child_id: String,
) -> Result<Vec<SpaceRoom>, ClientError> {
let child_id = RoomId::parse(child_id)?;
let parents = self.inner.joined_parents_of_child(&child_id).await;
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,
space_id: String,
) -> Result<(), ClientError> {
let space_id = RoomId::parse(space_id)?;
let child_id = RoomId::parse(child_id)?;
self.inner.add_child_to_space(child_id, space_id).await.map_err(ClientError::from)
}
pub async fn remove_child_from_space(
&self,
child_id: String,
space_id: String,
) -> Result<(), ClientError> {
let space_id = RoomId::parse(space_id)?;
let child_id = RoomId::parse(child_id)?;
self.inner.remove_child_from_space(child_id, space_id).await.map_err(ClientError::from)
}
/// Start a space leave process returning a [`LeaveSpaceHandle`] from which
/// rooms can be retrieved in reversed BFS order starting from the requested
/// `space_id` graph node. If the room is unknown then an error will be
/// returned.
///
/// Once the rooms to be left are chosen the handle can be used to leave
/// them.
pub async fn leave_space(
&self,
space_id: String,
) -> Result<Arc<LeaveSpaceHandle>, ClientError> {
let space_id = RoomId::parse(space_id)?;
let handle = self.inner.leave_space(&space_id).await.map_err(ClientError::from)?;
Ok(Arc::new(handle.into()))
}
}
/// 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
@@ -115,6 +209,27 @@ impl SpaceRoomList {
#[matrix_sdk_ffi_macros::export]
impl SpaceRoomList {
/// Returns the space of the room list if known.
pub fn space(&self) -> Option<SpaceRoom> {
self.inner.space().map(Into::into)
}
/// Subscribe to space updates.
pub fn subscribe_to_space_updates(
&self,
listener: Box<dyn SpaceRoomListSpaceListener>,
) -> Arc<TaskHandle> {
let space_updates = self.inner.subscribe_to_space_updates();
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
pin_mut!(space_updates);
while let Some(space) = space_updates.next().await {
listener.on_update(space.map(Into::into));
}
})))
}
/// Returns if the room list is currently paginating or not.
pub fn pagination_state(&self) -> SpaceRoomListPaginationState {
self.inner.pagination_state()
@@ -164,6 +279,23 @@ 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)]
pub trait SpaceRoomListSpaceListener: SendOutsideWasm + SyncOutsideWasm + Debug {
fn on_update(&self, space: Option<SpaceRoom>);
}
#[matrix_sdk_ffi_macros::export(callback_interface)]
@@ -181,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)]
@@ -189,8 +326,11 @@ pub struct SpaceRoom {
pub room_id: String,
/// The canonical alias of the room, if any.
pub canonical_alias: Option<String>,
/// The name of the room, if any.
pub name: Option<String>,
/// The room's name from the room state event if received from sync, or one
/// that's been computed otherwise.
pub display_name: String,
/// Room name as defined by the room state event only.
pub raw_name: Option<String>,
/// The topic of the room, if any.
pub topic: Option<String>,
/// The URL for the room's avatar, if one is set.
@@ -206,12 +346,19 @@ pub struct SpaceRoom {
/// Whether guest users may join the room and participate in it.
pub guest_can_join: bool,
/// Whether this room is a direct room.
///
/// Only set if the room is known to the client otherwise we
/// assume DMs shouldn't be exposed publicly in spaces.
pub is_direct: Option<bool>,
/// The number of children room this has, if a space.
pub children_count: u64,
/// Whether this room is joined, left etc.
pub state: Option<Membership>,
/// A list of room members considered to be heroes.
pub heroes: Option<Vec<RoomHero>>,
/// The via parameters of the room.
pub via: Vec<String>,
}
impl From<UISpaceRoom> for SpaceRoom {
@@ -219,7 +366,8 @@ impl From<UISpaceRoom> for SpaceRoom {
Self {
room_id: room.room_id.into(),
canonical_alias: room.canonical_alias.map(|alias| alias.into()),
name: room.name,
display_name: room.display_name,
raw_name: room.name,
topic: room.topic,
avatar_url: room.avatar_url.map(|url| url.into()),
room_type: room.room_type.into(),
@@ -227,9 +375,11 @@ impl From<UISpaceRoom> for SpaceRoom {
join_rule: room.join_rule.map(Into::into),
world_readable: room.world_readable,
guest_can_join: room.guest_can_join,
is_direct: room.is_direct,
children_count: room.children_count,
state: room.state.map(Into::into),
heroes: room.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
via: room.via.into_iter().map(Into::into).collect(),
}
}
}
@@ -274,3 +424,127 @@ 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.
///
/// Once the upstream client decides what rooms should actually be left, the
/// handle provides a method to execute that too.
#[derive(uniffi::Object)]
pub struct LeaveSpaceHandle {
inner: UILeaveSpaceHandle,
}
#[matrix_sdk_ffi_macros::export]
impl LeaveSpaceHandle {
/// A list of rooms to be left which next to normal [`SpaceRoom`] data also
/// include leave specific information.
pub fn rooms(&self) -> Vec<LeaveSpaceRoom> {
let rooms = self.inner.rooms();
rooms.iter().map(|room| room.clone().into()).collect()
}
/// Bulk leave the given rooms. Stops when encountering an error.
pub async fn leave(&self, room_ids: Vec<String>) -> Result<(), ClientError> {
let room_ids = room_ids.iter().map(RoomId::parse).collect::<Result<Vec<_>, _>>()?;
self.inner
.leave(|room| room_ids.contains(&room.space_room.room_id))
.await
.map_err(ClientError::from)
}
}
impl From<UILeaveSpaceHandle> for LeaveSpaceHandle {
fn from(handle: UILeaveSpaceHandle) -> Self {
LeaveSpaceHandle { inner: handle }
}
}
/// Space leaving specific room that groups normal [`SpaceRoom`] details with
/// information about the leaving user's role.
#[derive(uniffi::Record)]
pub struct LeaveSpaceRoom {
/// The underlying [`SpaceRoom`]
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.
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_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(),
}
}
}
+267
View File
@@ -0,0 +1,267 @@
#[cfg(feature = "sqlite")]
use std::path::PathBuf;
#[cfg(feature = "sqlite")]
use matrix_sdk::SqliteStoreConfig;
#[cfg(doc)]
use crate::client_builder::ClientBuilder;
/// The outcome of building a [`StoreBuilder`], with data that can be passed
/// directly to a [`ClientBuilder`].
pub enum StoreBuilderOutcome {
/// An SQLite store configuration successfully built.
#[cfg(feature = "sqlite")]
Sqlite { config: SqliteStoreConfig, cache_path: PathBuf, store_path: PathBuf },
/// An IndexedDB store configuration successfully built.
#[cfg(feature = "indexeddb")]
IndexedDb { name: String, passphrase: Option<String> },
/// An in-memory store configuration successfully built.
InMemory,
}
#[cfg(feature = "sqlite")]
mod sqlite {
use std::{fs, path::Path, sync::Arc};
use matrix_sdk::SqliteStoreConfig;
use tracing::debug;
use zeroize::Zeroizing;
use super::StoreBuilderOutcome;
use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc};
/// The store paths the client will use when built.
#[derive(Clone)]
struct StorePaths {
/// The path that the client will use to store its data.
data_path: String,
/// The path that the client will use to store its caches. This path can
/// be the same as the data path if you prefer to keep
/// everything in one place.
cache_path: String,
}
/// A builder for configuring a Sqlite session store.
#[derive(Clone, uniffi::Object)]
pub struct SqliteStoreBuilder {
paths: StorePaths,
passphrase: Zeroizing<Option<String>>,
pool_max_size: Option<usize>,
cache_size: Option<u32>,
journal_size_limit: Option<u32>,
system_is_memory_constrained: bool,
}
impl SqliteStoreBuilder {
pub(crate) fn raw_new(data_path: String, cache_path: String) -> Self {
Self {
paths: StorePaths { data_path, cache_path },
passphrase: Zeroizing::new(None),
pool_max_size: None,
cache_size: None,
journal_size_limit: None,
system_is_memory_constrained: false,
}
}
}
#[matrix_sdk_ffi_macros::export]
impl SqliteStoreBuilder {
/// Construct a [`SqliteStoreBuilder`] and set the paths that the client
/// will use to store its data and caches.
///
/// Both paths **must** be unique per session as the SDK stores aren't
/// capable of handling multiple users, however it is valid to use the
/// same path for both stores on a single session.
#[uniffi::constructor]
pub fn new(data_path: String, cache_path: String) -> Arc<Self> {
Arc::new(Self::raw_new(data_path, cache_path))
}
/// Set the passphrase for the stores.
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.passphrase = Zeroizing::new(passphrase);
Arc::new(builder)
}
/// Set the pool max size for the stores.
///
/// Each store exposes an async pool of connections. This method
/// controls the size of the pool. The larger the pool is, the more
/// memory is consumed, but also the more the app is reactive because it
/// doesn't need to wait on a pool to be available to run queries.
///
/// See [`SqliteStoreConfig::pool_max_size`] to learn more.
pub fn pool_max_size(self: Arc<Self>, pool_max_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.pool_max_size = pool_max_size.map(|size| {
size.try_into().expect("`pool_max_size` is too large to fit in `usize`")
});
Arc::new(builder)
}
/// Set the cache size for the stores.
///
/// Each store exposes a SQLite connection. This method controls the
/// cache size, in **bytes (!)**.
///
/// The cache represents data SQLite holds in memory at once per open
/// database file. The default cache implementation does not allocate
/// the full amount of cache memory all at once. Cache memory is
/// allocated in smaller chunks on an as-needed basis.
///
/// See [`SqliteStoreConfig::cache_size`] to learn more.
pub fn cache_size(self: Arc<Self>, cache_size: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.cache_size = cache_size;
Arc::new(builder)
}
/// Set the size limit for the SQLite WAL files of stores.
///
/// Each store uses the WAL journal mode. This method controls the size
/// limit of the WAL files, in **bytes (!)**.
///
/// See [`SqliteStoreConfig::journal_size_limit`] to learn more.
pub fn journal_size_limit(self: Arc<Self>, limit: Option<u32>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.journal_size_limit = limit;
Arc::new(builder)
}
/// Tell the client that the system is memory constrained, like in a
/// push notification process for example.
///
/// So far, at the time of writing (2025-04-07), it changes
/// the defaults of [`SqliteStoreConfig`]. Please check
/// [`SqliteStoreConfig::with_low_memory_config`].
pub fn system_is_memory_constrained(self: Arc<Self>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.system_is_memory_constrained = true;
Arc::new(builder)
}
}
impl SqliteStoreBuilder {
#[allow(clippy::result_large_err)]
pub fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
let data_path = Path::new(&self.paths.data_path);
let cache_path = Path::new(&self.paths.cache_path);
debug!(
data_path = %data_path.to_string_lossy(),
cache_path = %cache_path.to_string_lossy(),
"Creating directories for data and cache stores.",
);
fs::create_dir_all(data_path)?;
fs::create_dir_all(cache_path)?;
let mut sqlite_store_config = if self.system_is_memory_constrained {
SqliteStoreConfig::with_low_memory_config(data_path)
} else {
SqliteStoreConfig::new(data_path)
};
sqlite_store_config = sqlite_store_config.passphrase(self.passphrase.as_deref());
if let Some(size) = self.pool_max_size {
sqlite_store_config = sqlite_store_config.pool_max_size(size);
}
if let Some(size) = self.cache_size {
sqlite_store_config = sqlite_store_config.cache_size(size);
}
if let Some(limit) = self.journal_size_limit {
sqlite_store_config = sqlite_store_config.journal_size_limit(limit);
}
Ok(StoreBuilderOutcome::Sqlite {
config: sqlite_store_config,
store_path: data_path.to_owned(),
cache_path: cache_path.to_owned(),
})
}
}
}
#[cfg(feature = "indexeddb")]
mod indexeddb {
use std::sync::Arc;
use super::StoreBuilderOutcome;
use crate::{client_builder::ClientBuildError, helpers::unwrap_or_clone_arc};
#[derive(Clone, uniffi::Object)]
pub struct IndexedDbStoreBuilder {
name: String,
passphrase: Option<String>,
}
#[matrix_sdk_ffi_macros::export]
impl IndexedDbStoreBuilder {
#[uniffi::constructor]
pub fn new(name: String) -> Arc<Self> {
Arc::new(Self { name, passphrase: None })
}
/// Set the passphrase for the stores.
pub fn passphrase(self: Arc<Self>, passphrase: Option<String>) -> Arc<Self> {
let mut builder = unwrap_or_clone_arc(self);
builder.passphrase = passphrase;
Arc::new(builder)
}
}
impl IndexedDbStoreBuilder {
pub fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
Ok(StoreBuilderOutcome::IndexedDb {
name: self.name.clone(),
passphrase: self.passphrase.clone(),
})
}
}
}
#[cfg(feature = "indexeddb")]
pub use indexeddb::*;
#[cfg(feature = "sqlite")]
pub use sqlite::*;
use crate::client_builder::ClientBuildError;
/// Represent the kind of store the client will configure.
#[derive(Clone)]
pub enum StoreBuilder {
/// Represents the builder for the SQLite store.
#[cfg(feature = "sqlite")]
Sqlite(SqliteStoreBuilder),
/// Represents the builder for the IndexedDB store.
#[cfg(feature = "indexeddb")]
IndexedDb(IndexedDbStoreBuilder),
/// Represents the builder for in-memory store.
InMemory,
}
impl StoreBuilder {
#[allow(clippy::result_large_err)]
pub(crate) fn build(&self) -> Result<StoreBuilderOutcome, ClientBuildError> {
match self {
#[cfg(feature = "sqlite")]
Self::Sqlite(config) => config.build(),
#[cfg(feature = "indexeddb")]
Self::IndexedDb(config) => config.build(),
Self::InMemory => Ok(StoreBuilderOutcome::InMemory),
}
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ impl From<MatrixSyncServiceState> for SyncServiceState {
MatrixSyncServiceState::Idle => Self::Idle,
MatrixSyncServiceState::Running => Self::Running,
MatrixSyncServiceState::Terminated => Self::Terminated,
MatrixSyncServiceState::Error => Self::Error,
MatrixSyncServiceState::Error(_error) => Self::Error,
MatrixSyncServiceState::Offline => Self::Offline,
}
}
@@ -1,6 +1,9 @@
use std::sync::Arc;
use matrix_sdk_ui::timeline::event_type_filter::TimelineEventTypeFilter as InnerTimelineEventTypeFilter;
use matrix_sdk_ui::timeline::{
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
};
use ruma::{
events::{AnySyncTimelineEvent, TimelineEventType},
EventId,
@@ -12,31 +15,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)
}
@@ -61,6 +83,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 {
@@ -73,17 +120,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 {
@@ -94,18 +138,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| {
@@ -117,9 +157,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),
}
}
}
@@ -151,8 +189,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.
@@ -173,11 +211,11 @@ pub struct TimelineConfiguration {
pub date_divider_mode: DateDividerMode,
/// Should the read receipts and read markers be tracked for the timeline
/// items in this instance?
/// items in this instance and on which event types?
///
/// As this has a non negligible performance impact, make sure to enable it
/// only when you need it.
pub track_read_receipts: bool,
pub track_read_receipts: TimelineReadReceiptTracking,
/// Whether this timeline instance should report UTDs through the client's
/// delegate.
+178 -21
View File
@@ -16,9 +16,14 @@ use std::collections::HashMap;
use matrix_sdk::room::power_levels::power_level_user_changes;
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
use ruma::events::FullStateEventContent;
use ruma::events::{
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, FullStateEventContent,
};
use crate::{timeline::msg_like::MsgLikeContent, utils::Timestamp};
use crate::{
client::JoinRule, event::TimelineEventType, timeline::msg_like::MsgLikeContent,
utils::Timestamp,
};
impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent {
fn from(value: matrix_sdk_ui::timeline::TimelineItemContent) -> Self {
@@ -35,7 +40,7 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
Content::CallInvite => TimelineItemContent::CallInvite,
Content::CallNotify => TimelineItemContent::CallNotify,
Content::RtcNotification => TimelineItemContent::RtcNotification,
Content::MembershipChange(membership) => {
let reason = match membership.content() {
@@ -95,6 +100,51 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
}
}
#[derive(Debug, Clone, uniffi::Enum)]
pub enum HistoryVisibility {
/// Previous events are accessible to newly joined members from the point
/// they were invited onwards.
///
/// Events stop being accessible when the member' state changes to
/// something other than *invite* or *join*.
Invited,
/// Previous events are accessible to newly joined members from the point
/// they joined the room onwards.
/// Events stop being accessible when the member' state changes to
/// something other than *join*.
Joined,
/// Previous events are always accessible to newly joined members.
///
/// All events in the room are accessible, even those sent when the member
/// was not a part of the room.
Shared,
/// All events while this is the `HistoryVisibility` value may be shared by
/// any participating homeserver with anyone, regardless of whether they
/// have ever joined the room.
WorldReadable,
/// A custom history visibility, up for interpretation by the consumer.
Custom {
/// The string representation for this custom history visibility.
repr: String,
},
}
impl From<RumaHistoryVisibility> for HistoryVisibility {
fn from(value: RumaHistoryVisibility) -> Self {
match value {
RumaHistoryVisibility::Invited => Self::Invited,
RumaHistoryVisibility::Joined => Self::Joined,
RumaHistoryVisibility::Shared => Self::Shared,
RumaHistoryVisibility::WorldReadable => Self::WorldReadable,
_ => Self::Custom { repr: value.to_string() },
}
}
}
#[derive(Clone, uniffi::Enum)]
// A note about this `allow(clippy::large_enum_variant)`.
// In order to reduce the size of `TimelineItemContent`, we would need to
@@ -109,7 +159,7 @@ pub enum TimelineItemContent {
content: MsgLikeContent,
},
CallInvite,
CallNotify,
RtcNotification,
RoomMembership {
user_id: String,
user_display_name: Option<String>,
@@ -195,29 +245,69 @@ 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,
RoomCreate {
federate: Option<bool>,
},
RoomEncryption,
RoomGuestAccess,
RoomHistoryVisibility,
RoomJoinRules,
RoomName { name: Option<String> },
RoomPinnedEvents { change: RoomPinnedEventsChange },
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
RoomHistoryVisibility {
history_visibility: Option<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: Option<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 {
@@ -240,11 +330,39 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
Self::RoomAvatar { url }
}
Content::RoomCanonicalAlias(_) => Self::RoomCanonicalAlias,
Content::RoomCreate(_) => Self::RoomCreate,
Content::RoomCreate(c) => {
let federate = match c {
FullContent::Original { content, .. } => Some(content.federate),
FullContent::Redacted(_) => None,
};
Self::RoomCreate { federate }
}
Content::RoomEncryption(_) => Self::RoomEncryption,
Content::RoomGuestAccess(_) => Self::RoomGuestAccess,
Content::RoomHistoryVisibility(_) => Self::RoomHistoryVisibility,
Content::RoomJoinRules(_) => Self::RoomJoinRules,
Content::RoomHistoryVisibility(c) => {
let history_visibility = match c {
FullContent::Original { content, .. } => {
Some(content.history_visibility.clone().into())
}
FullContent::Redacted(_) => None,
};
Self::RoomHistoryVisibility { history_visibility }
}
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
}
}
}
FullContent::Redacted(_) => None,
};
Self::RoomJoinRules { join_rule }
}
Content::RoomName(c) => {
let name = match c {
FullContent::Original { content, .. } => Some(content.name.clone()),
@@ -255,17 +373,56 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
Content::RoomPinnedEvents(c) => Self::RoomPinnedEvents { change: c.into() },
Content::RoomPowerLevels(c) => match c {
FullContent::Original { content, prev_content } => 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: Some(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 }
}
FullContent::Redacted(_) => Self::RoomPowerLevels {
events: Default::default(),
previous_events: None,
users: Default::default(),
previous_users: None,
thresholds: None,
previous_thresholds: None,
},
},
Content::RoomServerAcl(_) => Self::RoomServerAcl,
Content::RoomThirdPartyInvite(c) => {
+110 -56
View File
@@ -21,7 +21,6 @@ use matrix_sdk::{
attachment::{
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
event_cache::RoomPaginationStatus,
room::edit::EditedContent as SdkEditedContent,
};
@@ -31,12 +30,15 @@ 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};
use ruma::{
assign,
events::{
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
poll::{
@@ -49,6 +51,7 @@ use ruma::{
},
room::message::{
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
TextMessageEventContent,
},
AnyMessageLikeEventContent,
},
@@ -111,16 +114,16 @@ impl Timeline {
.transpose()
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
let formatted_caption = formatted_body_from(
params.caption.as_deref(),
params.formatted_caption.map(Into::into),
);
let caption = params.caption.map(|caption| {
let formatted =
formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
assign!(TextMessageEventContent::plain(caption), { formatted })
});
let attachment_config = AttachmentConfig {
info: Some(attachment_info),
thumbnail,
caption: params.caption,
formatted_caption,
caption,
mentions: params.mentions.map(Into::into),
in_reply_to: in_reply_to_event_id,
..Default::default()
@@ -352,17 +355,31 @@ impl Timeline {
Ok(())
}
/// Mark the room as read by trying to attach an *unthreaded* read receipt
/// to the latest room event.
/// Mark the timeline as read by attempting to send a read receipt on the
/// latest visible event.
///
/// This works even if the latest event belongs to a thread, as a threaded
/// reply also belongs to the unthreaded timeline. No threaded receipt
/// will be sent here (see also #3123).
/// The latest visible event is determined from the timeline's focus kind
/// and whether or not it hides threaded events. If no latest event can
/// be determined and the timeline is live, the room's unread marker is
/// unset instead.
///
/// # Arguments
///
/// * `receipt_type` - The type of receipt to send. When using
/// [`ReceiptType::FullyRead`], an unthreaded receipt will be sent. This
/// works even if the latest event belongs to a thread, as a threaded
/// reply also belongs to the unthreaded timeline. Otherwise the receipt
/// thread will be determined based on the timeline's focus kind.
pub async fn mark_as_read(&self, receipt_type: ReceiptType) -> Result<(), ClientError> {
self.inner.mark_as_read(receipt_type.into()).await?;
Ok(())
}
/// Returns the latest [`EventId`] in the timeline.
pub async fn latest_event_id(&self) -> Option<String> {
self.inner.latest_event_id().await.as_deref().map(ToString::to_string)
}
/// Queues an event in the room's send queue so it's processed for
/// sending later.
///
@@ -422,14 +439,12 @@ impl Timeline {
self: Arc<Self>,
params: UploadParameters,
audio_info: AudioInfo,
waveform: Vec<u16>,
waveform: Vec<f32>,
) -> Result<Arc<SendAttachmentJoinHandle>, RoomError> {
let attachment_info = AttachmentInfo::Voice {
audio_info: BaseAudioInfo::try_from(&audio_info)
.map_err(|_| RoomError::InvalidAttachmentData)?,
waveform: Some(waveform),
};
self.send_attachment(params, attachment_info, audio_info.mimetype, None)
let mut info =
BaseAudioInfo::try_from(&audio_info).map_err(|_| RoomError::InvalidAttachmentData)?;
info.waveform = Some(waveform);
self.send_attachment(params, AttachmentInfo::Voice(info), audio_info.mimetype, None)
}
pub fn send_file(
@@ -605,13 +620,14 @@ impl Timeline {
///
/// Ensures that only one reaction is sent at a time to avoid race
/// conditions and spamming the homeserver with requests.
///
/// Returns `true` if the reaction was added, `false` if it was removed.
pub async fn toggle_reaction(
&self,
item_id: EventOrTransactionId,
key: String,
) -> Result<(), ClientError> {
self.inner.toggle_reaction(&item_id.try_into()?, &key).await?;
Ok(())
) -> Result<bool, ClientError> {
Ok(self.inner.toggle_reaction(&item_id.try_into()?, &key).await?)
}
pub async fn fetch_details_for_event(&self, event_id: String) -> Result<(), ClientError> {
@@ -707,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`
@@ -717,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(
@@ -964,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,
}
@@ -977,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,
}
}
@@ -995,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,
@@ -1018,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(),
@@ -1073,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,
@@ -1261,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.
@@ -1286,7 +1317,7 @@ impl LazyTimelineItemProvider {
}
/// Mimic the [`UiLatestEventValue`] type.
#[derive(Clone, uniffi::Enum)]
#[derive(uniffi::Enum)]
pub enum LatestEventValue {
None,
Remote {
@@ -1296,10 +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,
},
}
@@ -1316,8 +1354,21 @@ impl From<UiLatestEventValue> for LatestEventValue {
content: content.into(),
}
}
UiLatestEventValue::Local { timestamp, content, is_sending } => {
Self::Local { timestamp: timestamp.into(), content: content.into(), 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(),
state,
}
}
}
}
@@ -1336,7 +1387,7 @@ mod galleries {
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
use matrix_sdk_ui::timeline::GalleryConfig;
use mime::Mime;
use ruma::EventId;
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
use tokio::sync::Mutex;
use tracing::error;
@@ -1475,15 +1526,18 @@ mod galleries {
let mime_str = self.mimetype().as_ref().ok_or(RoomError::InvalidAttachmentMimeType)?;
let mime_type =
mime_str.parse::<Mime>().map_err(|_| RoomError::InvalidAttachmentMimeType)?;
let caption = self.caption().as_ref().map(|caption| {
let formatted = formatted_body_from(
Some(caption),
self.formatted_caption().clone().map(Into::into),
);
assign!(TextMessageEventContent::plain(caption), { formatted })
});
Ok(matrix_sdk_ui::timeline::GalleryItemInfo {
source: self.source().clone().into(),
content_type: mime_type,
attachment_info: self.attachment_info()?,
caption: self.caption().clone(),
formatted_caption: self
.formatted_caption()
.clone()
.map(ruma::events::room::message::FormattedBody::from),
caption,
thumbnail: self.thumbnail()?,
})
}
@@ -1542,10 +1596,11 @@ mod galleries {
params: GalleryUploadParameters,
item_infos: Vec<GalleryItemInfo>,
) -> Result<Arc<SendGalleryJoinHandle>, RoomError> {
let formatted_caption = formatted_body_from(
params.caption.as_deref(),
params.formatted_caption.map(Into::into),
);
let caption = params.caption.map(|caption| {
let formatted =
formatted_body_from(Some(&caption), params.formatted_caption.map(Into::into));
assign!(TextMessageEventContent::plain(caption), { formatted })
});
let in_reply_to = params
.in_reply_to
@@ -1555,8 +1610,7 @@ mod galleries {
.map_err(|_| RoomError::InvalidRepliedToEventId)?;
let mut gallery_config = GalleryConfig::new()
.caption(params.caption)
.formatted_caption(formatted_caption)
.caption(caption)
.mentions(params.mentions.map(Into::into))
.in_reply_to(in_reply_to);
@@ -14,7 +14,7 @@
use std::{collections::HashMap, sync::Arc};
use matrix_sdk::crypto::types::events::UtdCause;
use matrix_sdk_base::crypto::types::events::UtdCause;
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
use super::{
@@ -23,6 +23,7 @@ use super::{
};
use crate::{
error::ClientError,
event::MessageLikeEventType,
ruma::{ImageInfo, MediaSource, MediaSourceExt, Mentions, MessageType, PollKind},
timeline::content::ReactionSenderData,
utils::Timestamp,
@@ -50,6 +51,9 @@ pub enum MsgLikeKind {
/// An `m.room.encrypted` event that could not be decrypted.
UnableToDecrypt { msg: EncryptedMessage },
/// A custom message like event.
Other { event_type: MessageLikeEventType },
}
/// A special kind of [`super::TimelineItemContent`] that groups together
@@ -182,6 +186,15 @@ impl TryFrom<matrix_sdk_ui::timeline::MsgLikeContent> for MsgLikeContent {
thread_root,
thread_summary,
},
Kind::Other(other) => Self {
kind: MsgLikeKind::Other {
event_type: MessageLikeEventType::Other(other.event_type().to_string()),
},
reactions,
in_reply_to,
thread_root,
thread_summary,
},
})
}
}
@@ -242,6 +255,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]
@@ -260,6 +278,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()),
}
}
}
+52 -4
View File
@@ -1,10 +1,12 @@
#[cfg(feature = "sentry")]
use std::borrow::ToOwned;
use std::{
collections::BTreeMap,
sync::{Arc, Mutex},
};
use once_cell::sync::OnceCell;
use tracing::{callsite::DefaultCallsite, field::FieldSet, Callsite};
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
/// Log an event.
@@ -96,6 +98,8 @@ fn span_or_event_enabled(callsite: &'static DefaultCallsite) -> bool {
#[derive(uniffi::Object)]
pub struct Span(tracing::Span);
pub(crate) const BRIDGE_SPAN_NAME: &str = "<sdk_bridge_span>";
#[matrix_sdk_ffi_macros::export]
impl Span {
/// Create a span originating at the given callsite (file, line and column).
@@ -129,18 +133,41 @@ impl Span {
level: LogLevel,
target: String,
name: String,
bridge_trace_id: Option<String>,
) -> Arc<Self> {
static CALLSITES: Mutex<BTreeMap<MetadataId, &'static DefaultCallsite>> =
Mutex::new(BTreeMap::new());
let loc = MetadataId { file, line, level, target, name: Some(name) };
let callsite = get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN);
// If sentry isn't enabled, ignore bridge_trace_id's contents
let bridge_trace_id = if cfg!(feature = "sentry") { bridge_trace_id } else { None };
let callsite = if cfg!(feature = "sentry") {
get_or_init_metadata(&CALLSITES, loc, &["sentry", "sentry.trace"], MetadataKind::SPAN)
} else {
get_or_init_metadata(&CALLSITES, loc, &[], MetadataKind::SPAN)
};
let metadata = callsite.metadata();
let span = if span_or_event_enabled(callsite) {
// This function is hidden from docs, but we have to use it (see above).
let values = metadata.fields().value_set(&[]);
tracing::Span::new(metadata, &values)
let fields = metadata.fields();
if let Some(parent_trace_id) = bridge_trace_id {
debug!("Adding fields | sentry:true, sentry.trace={parent_trace_id}");
let sentry_field = fields.field("sentry").unwrap();
let sentry_trace_field = fields.field("sentry.trace").unwrap();
#[allow(trivial_casts)] // The compiler is lying, it can't infer this cast
let values = [
(&sentry_field, Some(&true as &dyn tracing::Value)),
(&sentry_trace_field, Some(&parent_trace_id as &dyn tracing::Value)),
];
tracing::Span::new(metadata, &fields.value_set(&values))
} else {
tracing::Span::new(metadata, &fields.value_set(&[]))
}
} else {
tracing::Span::none()
};
@@ -164,6 +191,27 @@ impl Span {
fn is_none(&self) -> bool {
self.0.is_none()
}
/// Creates a [`Span`] that acts as a bridge between the client spans and
/// the SDK ones, allowing them to be joined in Sentry. This function
/// will only return a valid span if the `sentry` feature is enabled,
/// otherwise it will return a noop span.
#[uniffi::constructor]
pub fn new_bridge_span(target: String, parent_trace_id: Option<String>) -> Arc<Self> {
if cfg!(feature = "sentry") {
Self::new(
"Bridge".to_owned(),
None,
LogLevel::Info,
target,
BRIDGE_SPAN_NAME.to_owned(),
parent_trace_id,
)
} else {
error!("Sentry is not enabled!");
Arc::new(Self(tracing::Span::none()))
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, uniffi::Enum)]
+1 -1
View File
@@ -14,7 +14,7 @@
use std::{fmt::Debug, sync::Arc, time::Duration};
use matrix_sdk::crypto::types::events::UtdCause;
use matrix_sdk_base::crypto::types::events::UtdCause;
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
use matrix_sdk_ui::unable_to_decrypt_hook::{
UnableToDecryptHook, UnableToDecryptInfo as SdkUnableToDecryptInfo,
+14 -3
View File
@@ -125,9 +125,10 @@ pub async fn generate_webview_url(
/// call widget.
#[matrix_sdk_ffi_macros::export]
pub fn new_virtual_element_call_widget(
props: matrix_sdk::widget::VirtualElementCallWidgetOptions,
props: matrix_sdk::widget::VirtualElementCallWidgetProperties,
config: matrix_sdk::widget::VirtualElementCallWidgetConfig,
) -> Result<WidgetSettings, ParseError> {
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props)
Ok(matrix_sdk::widget::WidgetSettings::new_virtual_element_call_widget(props, config)
.map(|w| w.into())?)
}
@@ -175,6 +176,10 @@ pub fn get_element_call_required_permissions(
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::RoomRedaction.to_string(),
},
// This allows declining an incoming call and detect if someone declines a call.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::RtcDecline.to_string(),
},
];
WidgetCapabilities {
@@ -199,10 +204,12 @@ pub fn get_element_call_required_permissions(
send: vec![
// To notify other users that a call has started.
WidgetEventFilter::MessageLikeWithType {
event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
event_type: MessageLikeEventType::RtcNotification.to_string(),
},
// Also for call notifications, except this is the deprecated fallback type which
// Element Call still sends.
// Deprecated for now, kept for backward compatibility as widgets will send both
// CallNotify and RtcNotification.
WidgetEventFilter::MessageLikeWithType {
event_type: MessageLikeEventType::CallNotify.to_string(),
},
@@ -528,5 +535,9 @@ mod tests {
);
cap_assert("org.matrix.msc2762.send.event:org.matrix.rageshake_request");
cap_assert("org.matrix.msc2762.send.event:io.element.call.encryption_keys");
// RTC decline
cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
}
}
+4 -1
View File
@@ -1,4 +1,7 @@
[bindings.kotlin]
package_name = "org.matrix.rustcomponents.sdk"
cdylib_name = "matrix_sdk_ffi"
android_cleaner = true
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
+76
View File
@@ -6,6 +6,82 @@ 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 `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))
### Refactor
- [**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)) (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
- [**breaking**] `ServerInfo` has been renamed to `SupportedVersionsResponse`,
and its `well_known` field has been removed. It is also wrapped in a
`TtlStoreValue` that handles the expiration of the data, rather than calling
`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.
- [**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`.
([#5707](https://github.com/matrix-org/matrix-rust-sdk/pull/5707))
### Features
- [**breaking**] The `EventCacheStore::get_room_events()` method has received
two new arguments. This allows users to load only events of a certain event
type and events that were encrypted using a certain room key identified by its
session ID.
([#5817](https://github.com/matrix-org/matrix-rust-sdk/pull/5817))
- `ComposerDraft` can now store attachments alongside text messages.
([#5794](https://github.com/matrix-org/matrix-rust-sdk/pull/5794))
## [0.14.1] - 2025-09-10
### 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)).
## [0.14.0] - 2025-09-04
### Features
+7 -8
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version.workspace = true
version = "0.14.0"
version = "0.16.0"
[package.metadata.docs.rs]
all-features = true
@@ -36,7 +36,7 @@ experimental-send-custom-to-device = [
# https://github.com/matrix-org/matrix-rust-sdk/issues/5397.
experimental-encrypted-state-events = [
"e2e-encryption",
"ruma/unstable-msc3414",
"ruma/unstable-msc4362",
"matrix-sdk-crypto?/experimental-encrypted-state-events"
]
@@ -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",
@@ -65,13 +62,15 @@ testing = [
# Add support for inline media galleries via msgtypes
unstable-msc4274 = []
experimental-element-recent-emojis = []
[dependencies]
as_variant.workspace = true
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
@@ -82,9 +81,8 @@ 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 = "1.11.2"
regex.workspace = true
ruma = { workspace = true, features = [
"canonical-json",
"unstable-msc2867",
"unstable-msc3381",
"unstable-msc4186",
@@ -107,6 +105,7 @@ futures-executor.workspace = true
http.workspace = true
matrix-sdk-test.workspace = true
matrix-sdk-test-utils.workspace = true
proptest.workspace = true
similar-asserts.workspace = true
stream_assert.workspace = true
+67 -97
View File
@@ -57,7 +57,8 @@ use crate::{
InviteAcceptanceDetails, RoomStateFilter, SessionMeta,
deserialized_responses::DisplayName,
error::{Error, Result},
event_cache::store::EventCacheStoreLock,
event_cache::store::{EventCacheStoreLock, EventCacheStoreLockState},
media::store::MediaStoreLock,
response_processors::{self as processors, Context},
room::{
Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
@@ -92,6 +93,9 @@ pub struct BaseClient {
/// The store used by the event cache.
event_cache_store: EventCacheStoreLock,
/// The store used by the media cache.
media_store: MediaStoreLock,
/// The store used for encryption.
///
/// This field is only meant to be used for `OlmMachine` initialization.
@@ -108,10 +112,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")]
@@ -175,27 +175,15 @@ 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,
media_store: config.media_store,
#[cfg(feature = "e2e-encryption")]
crypto_store: config.crypto_store,
#[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")]
@@ -223,6 +211,7 @@ impl BaseClient {
let copy = Self {
state_store: BaseStateStore::new(config.state_store),
event_cache_store: config.event_cache_store,
media_store: config.media_store,
// We copy the crypto store as well as the `OlmMachine` for two reasons:
// 1. The `self.crypto_store` is the same as the one used inside the `OlmMachine`.
// 2. We need to ensure that the parent and child use the same data and caches inside
@@ -232,16 +221,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)
}
@@ -290,11 +276,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.
@@ -307,6 +289,11 @@ impl BaseClient {
&self.event_cache_store
}
/// Get a reference to the media store.
pub fn media_store(&self) -> &MediaStoreLock {
&self.media_store
}
/// Check whether the client has been activated.
///
/// See [`BaseClient::activate`] to know what it means.
@@ -355,13 +342,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);
@@ -407,14 +388,10 @@ 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 _sync_lock = self.sync_lock().lock().await;
let _state_store_lock = self.state_store_lock().lock().await;
let mut room_info = room.clone_info();
room_info.mark_as_knocked();
@@ -472,16 +449,12 @@ 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.
if room.state() != RoomState::Joined {
let _sync_lock = self.sync_lock().lock().await;
let _state_store_lock = self.state_store_lock().lock().await;
let mut room_info = room.clone_info();
let previous_state = room.state();
@@ -525,14 +498,10 @@ 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 _sync_lock = self.sync_lock().lock().await;
let _state_store_lock = self.state_store_lock().lock().await;
let mut room_info = room.clone_info();
room_info.mark_as_left();
@@ -547,9 +516,12 @@ impl BaseClient {
Ok(())
}
/// Get access to the store's sync lock.
pub fn sync_lock(&self) -> &Mutex<()> {
self.state_store.sync_lock()
/// Get a lock to the state store, with an exclusive access.
///
/// It doesn't give an access to the state store itself. It's rather a lock
/// to synchronise all accesses to the state store.
pub fn state_store_lock(&self) -> &Mutex<()> {
self.state_store.lock()
}
/// Receive a response from a sync call.
@@ -591,41 +563,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
@@ -673,7 +630,6 @@ impl BaseClient {
&mut context,
processors::room::RoomCreationData::new(
&room_id,
self.room_info_notable_update_sender.clone(),
requested_required_states,
&mut ambiguity_cache,
),
@@ -701,7 +657,6 @@ impl BaseClient {
&mut context,
processors::room::RoomCreationData::new(
&room_id,
self.room_info_notable_update_sender.clone(),
requested_required_states,
&mut ambiguity_cache,
),
@@ -727,8 +682,8 @@ 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,
@@ -744,8 +699,8 @@ 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,
@@ -772,7 +727,7 @@ impl BaseClient {
context.state_changes.ambiguity_maps = ambiguity_cache.cache;
{
let _sync_lock = self.sync_lock().lock().await;
let _state_store_lock = self.state_store_lock().lock().await;
processors::changes::save_and_apply(
context,
@@ -795,7 +750,11 @@ impl BaseClient {
.await;
// Save the new display name updates if any.
processors::changes::save_only(context, &self.state_store).await?;
{
let _state_store_lock = self.state_store_lock().lock().await;
processors::changes::save_only(context, &self.state_store).await?;
}
for (room_id, member_ids) in updated_members_in_room {
if let Some(room) = self.get_room(&room_id) {
@@ -916,18 +875,21 @@ impl BaseClient {
context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
let _sync_lock = self.sync_lock().lock().await;
let mut room_info = room.clone_info();
room_info.mark_members_synced();
context.state_changes.add_room(room_info);
{
let _state_store_lock = self.state_store_lock().lock().await;
processors::changes::save_and_apply(
context,
&self.state_store,
&self.ignore_user_list_changes,
None,
)
.await?;
let mut room_info = room.clone_info();
room_info.mark_members_synced();
context.state_changes.add_room(room_info);
processors::changes::save_and_apply(
context,
&self.state_store,
&self.ignore_user_list_changes,
None,
)
.await?;
}
let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
@@ -1041,7 +1003,15 @@ impl BaseClient {
self.state_store.forget_room(room_id).await?;
// Remove the room in the event cache store too.
self.event_cache_store().lock().await?.remove_room(room_id).await?;
match self.event_cache_store().lock().await? {
// If the lock is clear, we can do the operation as expected.
// If the lock is dirty, we can ignore to refresh the state, we just need to remove a
// room. Also, we must not mark the lock as non-dirty because other operations may be
// critical and may need to refresh the `EventCache`' state.
EventCacheStoreLockState::Clean(guard) | EventCacheStoreLockState::Dirty(guard) => {
guard.remove_room(room_id).await?
}
}
Ok(())
}
@@ -1091,7 +1061,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.
@@ -20,7 +20,8 @@ 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,
@@ -486,16 +487,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.
+2 -2
View File
@@ -15,7 +15,7 @@
//! Error conditions.
use matrix_sdk_common::store_locks::LockStoreError;
use matrix_sdk_common::cross_process_lock::CrossProcessLockError;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
use thiserror::Error;
@@ -51,7 +51,7 @@ pub enum Error {
/// An error happened while attempting to lock the event cache store.
#[error(transparent)]
EventCacheLock(#[from] LockStoreError),
EventCacheLock(#[from] CrossProcessLockError),
/// An error occurred in the crypto store.
#[cfg(feature = "e2e-encryption")]
File diff suppressed because it is too large Load Diff
@@ -14,90 +14,53 @@
use std::{
collections::HashMap,
num::NonZeroUsize,
sync::{Arc, RwLock as StdRwLock},
};
use async_trait::async_trait;
use matrix_sdk_common::{
cross_process_lock::{
CrossProcessLockGeneration,
memory_store_helper::{Lease, try_take_leased_lock},
},
linked_chunk::{
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
RawChunk, Update, relational::RelationalLinkedChunk,
},
ring_buffer::RingBuffer,
store_locks::memory_store_helper::try_take_leased_lock,
};
use ruma::{
EventId, MxcUri, OwnedEventId, OwnedMxcUri, RoomId,
events::relation::RelationType,
time::{Instant, SystemTime},
};
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
use tracing::error;
use super::{
EventCacheStore, EventCacheStoreError, Result, compute_filters_string, extract_event_relation,
media::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService},
};
use crate::{
event_cache::{Event, Gap},
media::{MediaRequestParameters, UniqueKey as _},
};
use super::{EventCacheStore, EventCacheStoreError, Result, extract_event_relation};
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>>,
media_service: MediaService,
}
#[derive(Debug)]
struct MemoryStoreInner {
media: RingBuffer<MediaContent>,
leases: HashMap<String, (String, Instant)>,
leases: HashMap<String, Lease>,
events: RelationalLinkedChunk<OwnedEventId, Event, Gap>,
media_retention_policy: Option<MediaRetentionPolicy>,
last_media_cleanup_time: SystemTime,
}
/// A media content in the `MemoryStore`.
#[derive(Debug)]
struct MediaContent {
/// The URI of the content.
uri: OwnedMxcUri,
/// The unique key of the content.
key: String,
/// The bytes of the content.
data: Vec<u8>,
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
ignore_policy: bool,
/// The time of the last access of the content.
last_access: SystemTime,
}
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
impl Default for MemoryStore {
fn default() -> Self {
// Given that the store is empty, we won't need to clean it up right away.
let last_media_cleanup_time = SystemTime::now();
let media_service = MediaService::new();
media_service.restore(None, Some(last_media_cleanup_time));
Self {
inner: Arc::new(StdRwLock::new(MemoryStoreInner {
media: RingBuffer::new(NUMBER_OF_MEDIAS),
leases: Default::default(),
events: RelationalLinkedChunk::new(),
media_retention_policy: None,
last_media_cleanup_time,
})),
media_service,
}
}
}
@@ -119,7 +82,7 @@ impl EventCacheStore for MemoryStore {
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
let mut inner = self.inner.write().unwrap();
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
@@ -131,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(Box::new(e)))?;
Ok(())
}
@@ -238,14 +204,13 @@ impl EventCacheStore for MemoryStore {
) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
let inner = self.inner.read().unwrap();
let filters = compute_filters_string(filters);
let related_events = inner
.events
.items(room_id)
.filter_map(|(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());
// Must relate to the target item.
if related_to != event_id {
@@ -264,6 +229,28 @@ impl EventCacheStore for MemoryStore {
Ok(related_events)
}
async fn get_room_events(
&self,
room_id: &RoomId,
event_type: Option<&str>,
session_id: Option<&str>,
) -> Result<Vec<Event>, Self::Error> {
let inner = self.inner.read().unwrap();
let event: Vec<_> = inner
.events
.items(room_id)
.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();
Ok(event)
}
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
if event.event_id().is_none() {
error!(%room_id, "Trying to save an event with no ID");
@@ -273,318 +260,26 @@ impl EventCacheStore for MemoryStore {
Ok(())
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<()> {
self.media_service.add_media_content(self, request, data, ignore_policy).await
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
let expected_key = from.unique_key();
let mut inner = self.inner.write().unwrap();
if let Some(media_content) =
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
{
media_content.uri = to.uri().to_owned();
media_content.key = to.unique_key();
}
async fn optimize(&self) -> Result<(), Self::Error> {
Ok(())
}
async fn get_media_content(&self, request: &MediaRequestParameters) -> Result<Option<Vec<u8>>> {
self.media_service.get_media_content(self, request).await
}
async fn remove_media_content(&self, request: &MediaRequestParameters) -> Result<()> {
let expected_key = request.unique_key();
let mut inner = self.inner.write().unwrap();
let Some(index) =
inner.media.iter().position(|media_content| media_content.key == expected_key)
else {
return Ok(());
};
inner.media.remove(index);
Ok(())
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.media_service.get_media_content_for_uri(self, uri).await
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut inner = self.inner.write().unwrap();
let positions = inner
.media
.iter()
.enumerate()
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
inner.media.remove(position);
}
Ok(())
}
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.set_media_retention_policy(self, policy).await
}
fn media_retention_policy(&self) -> MediaRetentionPolicy {
self.media_service.media_retention_policy()
}
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
}
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
self.media_service.clean_up_media_cache(self).await
}
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl EventCacheStoreMedia for MemoryStore {
type Error = EventCacheStoreError;
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
Ok(self.inner.read().unwrap().media_retention_policy)
}
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.inner.write().unwrap().media_retention_policy = Some(policy);
Ok(())
}
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
last_access: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
let ignore_policy = ignore_policy.is_yes();
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
// Do not store it.
return Ok(());
}
// Now, let's add it.
let mut inner = self.inner.write().unwrap();
inner.media.push(MediaContent {
uri: request.uri().to_owned(),
key: request.unique_key(),
data,
ignore_policy,
last_access,
});
Ok(())
}
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
let expected_key = request.unique_key();
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
{
media_content.ignore_policy = ignore_policy.is_yes();
}
Ok(())
}
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner.write().unwrap();
let expected_key = request.unique_key();
// First get the content out of the buffer, we are going to put it back at the
// end.
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
return Ok(None);
};
let Some(mut content) = inner.media.remove(index) else {
return Ok(None);
};
// Clone the data.
let data = content.data.clone();
// Update the last access time.
content.last_access = current_time;
// Put it back in the buffer.
inner.media.push(content);
Ok(Some(data))
}
async fn get_media_content_for_uri_inner(
&self,
expected_uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner.write().unwrap();
// First get the content out of the buffer, we are going to put it back at the
// end.
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
return Ok(None);
};
let Some(mut content) = inner.media.remove(index) else {
return Ok(None);
};
// Clone the data.
let data = content.data.clone();
// Update the last access time.
content.last_access = current_time;
// Put it back in the buffer.
inner.media.push(content);
Ok(Some(data))
}
async fn clean_up_media_cache_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error> {
if !policy.has_limitations() {
// We can safely skip all the checks.
return Ok(());
}
let mut inner = self.inner.write().unwrap();
// First, check media content that exceed the max filesize.
if policy.computed_max_file_size().is_some() {
inner.media.retain(|content| {
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
});
}
// Then, clean up expired media content.
if policy.last_access_expiry.is_some() {
inner.media.retain(|content| {
content.ignore_policy
|| !policy.has_content_expired(current_time, content.last_access)
});
}
// Finally, if the cache size is too big, remove old items until it fits.
if let Some(max_cache_size) = policy.max_cache_size {
// Reverse the iterator because in case the cache size is overflowing, we want
// to count the number of old items to remove. Items are sorted by last access
// and old items are at the start.
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|(mut cache_size, mut items_to_remove), (index, content)| {
if content.ignore_policy {
// Do not count it.
return (cache_size, items_to_remove);
}
let remove_item = if items_to_remove.is_empty() {
// We have not reached the max cache size yet.
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
cache_size = sum;
// Start removing items if we have exceeded the max cache size.
cache_size > max_cache_size
} else {
// The cache size is overflowing, remove the remaining items, since the
// max cache size cannot be bigger than
// usize::MAX.
true
}
} else {
// We have reached the max cache size already, just remove it.
true
};
if remove_item {
items_to_remove.push(index);
}
(cache_size, items_to_remove)
},
);
// The indexes are already in reverse order so we can just iterate in that order
// to remove them starting by the end.
for index in items_to_remove {
inner.media.remove(index);
}
}
inner.last_media_cleanup_time = current_time;
Ok(())
}
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
Ok(None)
}
}
#[cfg(test)]
#[allow(unused_imports)] // There seems to be a false positive when importing the test macros.
mod tests {
use super::{MemoryStore, Result};
use crate::event_cache_store_media_integration_tests;
use crate::{event_cache_store_integration_tests, event_cache_store_integration_tests_time};
async fn get_event_cache_store() -> Result<MemoryStore> {
Ok(MemoryStore::new())
}
event_cache_store_integration_tests!();
#[cfg(not(target_family = "wasm"))]
event_cache_store_integration_tests_time!();
event_cache_store_media_integration_tests!(with_media_size_tests);
}
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! The event cache stores holds events and downloaded media when the cache was
//! The event cache stores holds events when the cache was
//! activated to save bandwidth at the cost of increased storage space usage.
//!
//! Implementing the `EventCacheStore` trait, you can plug any storage backend
@@ -24,19 +24,15 @@ use std::{fmt, ops::Deref, str::Utf8Error, sync::Arc};
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
pub mod media;
mod memory_store;
mod traits;
use matrix_sdk_common::store_locks::{
BackingStore, CrossProcessStoreLock, CrossProcessStoreLockGuard, LockStoreError,
use matrix_sdk_common::cross_process_lock::{
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
MappedCrossProcessLockState, TryLock,
};
pub use matrix_sdk_store_encryption::Error as StoreEncryptionError;
use ruma::{
OwnedEventId,
events::{AnySyncTimelineEvent, relation::RelationType},
serde::Raw,
};
use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw};
use tracing::trace;
#[cfg(any(test, feature = "testing"))]
@@ -50,7 +46,7 @@ pub use self::{
#[derive(Clone)]
pub struct EventCacheStoreLock {
/// The inner cross process lock that is used to lock the `EventCacheStore`.
cross_process_lock: Arc<CrossProcessStoreLock<LockableEventCacheStore>>,
cross_process_lock: Arc<CrossProcessLock<LockableEventCacheStore>>,
/// The store itself.
///
@@ -69,7 +65,7 @@ impl EventCacheStoreLock {
/// Create a new lock around the [`EventCacheStore`].
///
/// The `holder` argument represents the holder inside the
/// [`CrossProcessStoreLock::new`].
/// [`CrossProcessLock::new`].
pub fn new<S>(store: S, holder: String) -> Self
where
S: IntoEventCacheStore,
@@ -77,7 +73,7 @@ impl EventCacheStoreLock {
let store = store.into_event_cache_store();
Self {
cross_process_lock: Arc::new(CrossProcessStoreLock::new(
cross_process_lock: Arc::new(CrossProcessLock::new(
LockableEventCacheStore(store.clone()),
"default".to_owned(),
holder,
@@ -86,38 +82,62 @@ impl EventCacheStoreLock {
}
}
/// Acquire a spin lock (see [`CrossProcessStoreLock::spin_lock`]).
pub async fn lock(&self) -> Result<EventCacheStoreLockGuard<'_>, LockStoreError> {
let cross_process_lock_guard = self.cross_process_lock.spin_lock(None).await?;
/// 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(EventCacheStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
Ok(lock_state)
}
}
/// The equivalent of [`CrossProcessLockState`] but for the [`EventCacheStore`].
///
/// [`CrossProcessLockState`]: matrix_sdk_common::cross_process_lock::CrossProcessLockState
pub type EventCacheStoreLockState = MappedCrossProcessLockState<EventCacheStoreLockGuard>;
/// An RAII implementation of a “scoped lock” of an [`EventCacheStoreLock`].
/// When this structure is dropped (falls out of scope), the lock will be
/// unlocked.
pub struct EventCacheStoreLockGuard<'a> {
#[derive(Clone)]
pub struct EventCacheStoreLockGuard {
/// The cross process lock guard.
#[allow(unused)]
cross_process_lock_guard: CrossProcessStoreLockGuard,
cross_process_lock_guard: CrossProcessLockGuard,
/// A reference to the store.
store: &'a DynEventCacheStore,
store: Arc<DynEventCacheStore>,
}
impl EventCacheStoreLockGuard {
/// Forward to [`CrossProcessLockGuard::clear_dirty`].
///
/// This is an associated method to avoid colliding with the [`Deref`]
/// implementation.
pub fn clear_dirty(this: &Self) {
this.cross_process_lock_guard.clear_dirty();
}
/// Force to [`CrossProcessLockGuard::is_dirty`].
pub fn is_dirty(this: &Self) -> bool {
this.cross_process_lock_guard.is_dirty()
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for EventCacheStoreLockGuard<'_> {
impl fmt::Debug for EventCacheStoreLockGuard {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("EventCacheStoreLockGuard").finish_non_exhaustive()
}
}
impl Deref for EventCacheStoreLockGuard<'_> {
impl Deref for EventCacheStoreLockGuard {
type Target = DynEventCacheStore;
fn deref(&self) -> &Self::Target {
self.store
self.store.as_ref()
}
}
@@ -177,15 +197,21 @@ impl EventCacheStoreError {
}
}
impl From<EventCacheStoreError> for CrossProcessLockError {
fn from(value: EventCacheStoreError) -> Self {
Self::TryLock(Box::new(value))
}
}
/// An `EventCacheStore` specific result type.
pub type Result<T, E = EventCacheStoreError> = std::result::Result<T, E>;
/// A type that wraps the [`EventCacheStore`] but implements [`BackingStore`] to
/// A type that wraps the [`EventCacheStore`] but implements [`TryLock`] to
/// make it usable inside the cross process lock.
#[derive(Clone, Debug)]
struct LockableEventCacheStore(Arc<DynEventCacheStore>);
impl BackingStore for LockableEventCacheStore {
impl TryLock for LockableEventCacheStore {
type LockError = EventCacheStoreError;
async fn try_lock(
@@ -193,7 +219,7 @@ impl BackingStore for LockableEventCacheStore {
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> std::result::Result<bool, Self::LockError> {
) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
}
@@ -226,18 +252,3 @@ pub fn extract_event_relation(event: &Raw<AnySyncTimelineEvent>) -> Option<(Owne
}
}
}
/// Compute the list of string filters to be applied when looking for an event's
/// relations.
// TODO: get Ruma fix from https://github.com/ruma/ruma/pull/2052, and get rid of this function
// then.
pub fn compute_filters_string(filters: Option<&[RelationType]>) -> Option<Vec<String>> {
filters.map(|filter| {
filter
.iter()
.map(|f| {
if *f == RelationType::Replacement { "m.replace".to_owned() } else { f.to_string() }
})
.collect()
})
}
@@ -17,21 +17,16 @@ use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{
AsyncTraitDeps,
cross_process_lock::CrossProcessLockGeneration,
linked_chunk::{
ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
RawChunk, Update,
},
};
use ruma::{EventId, MxcUri, OwnedEventId, RoomId, events::relation::RelationType};
use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType};
use super::{
EventCacheStoreError,
media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy},
};
use crate::{
event_cache::{Event, Gap},
media::MediaRequestParameters,
};
use super::EventCacheStoreError;
use crate::event_cache::{Event, Gap};
/// A default capacity for linked chunks, when manipulating in conjunction with
/// an `EventCacheStore` implementation.
@@ -52,7 +47,7 @@ pub trait EventCacheStore: AsyncTraitDeps {
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error>;
) -> Result<Option<CrossProcessLockGeneration>, Self::Error>;
/// An [`Update`] reflects an operation that has happened inside a linked
/// chunk. The linked chunk is used by the event cache to store the events
@@ -160,6 +155,17 @@ pub trait EventCacheStore: AsyncTraitDeps {
filter: Option<&[RelationType]>,
) -> Result<Vec<(Event, Option<Position>)>, Self::Error>;
/// Get all events in this room.
///
/// This method must return events saved either in any linked chunks, *or*
/// events saved "out-of-band" with the [`Self::save_event`] method.
async fn get_room_events(
&self,
room_id: &RoomId,
event_type: Option<&str>,
session_id: Option<&str>,
) -> Result<Vec<Event>, Self::Error>;
/// Save an event, that might or might not be part of an existing linked
/// chunk.
///
@@ -170,128 +176,16 @@ pub trait EventCacheStore: AsyncTraitDeps {
/// without causing an error.
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error>;
/// Add a media file's content in the media store.
/// Perform database optimizations if any are available, i.e. vacuuming in
/// SQLite.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// **Warning:** this was added to check if SQLite fragmentation was the
/// source of performance issues, **DO NOT use in production**.
#[doc(hidden)]
async fn optimize(&self) -> Result<(), Self::Error>;
/// Replaces the given media's content key with another one.
///
/// This should be used whenever a temporary (local) MXID has been used, and
/// it must now be replaced with its actual remote counterpart (after
/// uploading some content, or creating an empty MXC URI).
///
/// ⚠ No check is performed to ensure that the media formats are consistent,
/// i.e. it's possible to update with a thumbnail key a media that was
/// keyed as a file before. The caller is responsible of ensuring that
/// the replacement makes sense, according to their use case.
///
/// This should not raise an error when the `from` parameter points to an
/// unknown media, and it should silently continue in this case.
///
/// # Arguments
///
/// * `from` - The previous `MediaRequest` of the file.
///
/// * `to` - The new `MediaRequest` of the file.
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// In theory, there could be several files stored using the same URI and a
/// different `MediaFormat`. This API is meant to be used with a media file
/// that has only been stored with a single format.
///
/// If there are several media files for a given URI in different formats,
/// this API will only return one of them. Which one is left as an
/// implementation detail.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
async fn get_media_content_for_uri(&self, uri: &MxcUri)
-> Result<Option<Vec<u8>>, Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// This should not raise an error when the `uri` parameter points to an
/// unknown media, and it should return an Ok result in this case.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
/// keep media content.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` to use.
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Get the current `MediaRetentionPolicy`.
fn media_retention_policy(&self) -> MediaRetentionPolicy;
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Clean up the media cache with the current `MediaRetentionPolicy`.
///
/// If there is already an ongoing cleanup, this is a noop.
async fn clean_up_media_cache(&self) -> Result<(), Self::Error>;
/// Returns the size of the store in bytes, if known.
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
}
#[repr(transparent)]
@@ -314,7 +208,7 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<bool, Self::Error> {
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
}
@@ -387,73 +281,26 @@ impl<T: EventCacheStore> EventCacheStore for EraseEventCacheStoreError<T> {
self.0.find_event_relations(room_id, event_id, filter).await.map_err(Into::into)
}
async fn get_room_events(
&self,
room_id: &RoomId,
event_type: Option<&str>,
session_id: Option<&str>,
) -> Result<Vec<Event>, Self::Error> {
self.0.get_room_events(room_id, event_type, session_id).await.map_err(Into::into)
}
async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
self.0.save_event(room_id, event).await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
async fn optimize(&self) -> Result<(), Self::Error> {
self.0.optimize().await.map_err(Into::into)?;
Ok(())
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.replace_media_key(from, to).await.map_err(Into::into)
}
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
}
fn media_retention_policy(&self) -> MediaRetentionPolicy {
self.0.media_retention_policy()
}
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
}
async fn clean_up_media_cache(&self) -> Result<(), Self::Error> {
self.0.clean_up_media_cache().await.map_err(Into::into)
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
Ok(self.0.get_size().await.map_err(Into::into)?)
}
}
+234 -669
View File
@@ -1,27 +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, notify::SyncCallNotifyEvent},
poll::unstable_start::SyncUnstablePollStartEvent,
relation::RelationType,
room::{
member::{MembershipState, SyncRoomMemberEvent},
message::{MessageType, SyncRoomMessageEvent},
power_levels::RoomPowerLevels,
},
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)]
@@ -33,14 +16,114 @@ 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),
}
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 [`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),
}
}
/// Check whether the [`LatestEventValue`] represents a local value or not,
/// i.e. it is [`LocalIsSending`] or [`LocalCannotBeSent`].
///
/// [`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::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,
}
}
}
/// Represents the value for [`LatestEventValue::Remote`].
pub type RemoteLatestEventValue = TimelineEvent;
@@ -55,675 +138,157 @@ pub struct LocalLatestEventValue {
pub content: SerializableEventContent,
}
/// 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
YesCallNotify(&'a SyncCallNotifyEvent),
/// 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::CallNotify(notify)) => {
PossibleLatestEvent::YesCallNotify(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;
#[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;
use ruma::serde::Raw;
#[cfg(feature = "e2e-encryption")]
mod tests_latest_event_value {
use ruma::{
MilliSecondsSinceUnixEpoch, UInt, VoipVersionId,
events::{
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, EmptyStateKey,
Mentions, MessageLikeUnsigned, OriginalSyncMessageLikeEvent, OriginalSyncStateEvent,
RedactedSyncMessageLikeEvent, RedactedUnsigned, StateUnsigned, SyncMessageLikeEvent,
call::{
SessionDescription,
invite::{CallInviteEventContent, SyncCallInviteEvent},
notify::{
ApplicationType, CallNotifyEventContent, NotifyType, SyncCallNotifyEvent,
},
},
poll::{
unstable_response::{
SyncUnstablePollResponseEvent, UnstablePollResponseEventContent,
},
unstable_start::{
NewUnstablePollStartEventContent, SyncUnstablePollStartEvent,
UnstablePollAnswer, UnstablePollStartContentBlock,
},
},
relation::Replacement,
room::{
ImageInfo, MediaSource,
encrypted::{
EncryptedEventScheme, OlmV1Curve25519AesSha2Content, RoomEncryptedEventContent,
SyncRoomEncryptedEvent,
},
message::{
ImageMessageEventContent, MessageType, RedactedRoomMessageEventContent,
Relation, RoomMessageEventContent, SyncRoomMessageEvent,
},
topic::{RoomTopicEventContent, SyncRoomTopicEvent},
},
sticker::{StickerEventContent, SyncStickerEvent},
},
owned_event_id, owned_mxc_uri, owned_user_id,
MilliSecondsSinceUnixEpoch,
events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},
owned_event_id,
serde::Raw,
uint,
};
use serde_json::json;
use super::LatestEvent;
#[cfg(feature = "e2e-encryption")]
use super::{PossibleLatestEvent, is_suitable_for_latest_event};
use super::{LatestEventValue, LocalLatestEventValue, RemoteLatestEventValue};
use crate::store::SerializableEventContent;
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_room_messages_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomMessageEventContent::new(MessageType::Image(
ImageMessageEventContent::new(
"".to_owned(),
MediaSource::Plain(owned_mxc_uri!("mxc://example.com/1")),
),
)),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::YesRoomMessage(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event, None)
);
fn test_timestamp_with_none() {
let value = LatestEventValue::None;
assert_eq!(m.content.msgtype.msgtype(), "m.image");
assert_eq!(value.timestamp(), None);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_polls_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollStart(
SyncUnstablePollStartEvent::Original(OriginalSyncMessageLikeEvent {
content: NewUnstablePollStartEventContent::new(UnstablePollStartContentBlock::new(
"do you like rust?",
vec![UnstablePollAnswer::new("id", "yes")].try_into().unwrap(),
fn test_timestamp_with_remote() {
let value = LatestEventValue::Remote(RemoteLatestEventValue::from_plaintext(
Raw::from_json_string(
json!({
"content": RoomMessageEventContent::text_plain("raclette"),
"type": "m.room.message",
"event_id": "$ev0",
"room_id": "!r0",
"origin_server_ts": 42,
"sender": "@mnt_io:matrix.org",
})
.to_string(),
)
.unwrap(),
));
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
#[test]
fn test_timestamp_with_local_is_sending() {
let value = LatestEventValue::LocalIsSending(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_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"),
))
.into(),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::YesPoll(SyncMessageLikeEvent::Original(m)) =
is_suitable_for_latest_event(&event, None)
);
assert_eq!(m.content.poll_start().question.text, "do you like rust?");
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_invites_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallInvite(
SyncCallInviteEvent::Original(OriginalSyncMessageLikeEvent {
content: CallInviteEventContent::new(
"call_id".into(),
UInt::new(123).unwrap(),
SessionDescription::new("".into(), "".into()),
VoipVersionId::V1,
),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::YesCallInvite(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_call_notifications_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::CallNotify(
SyncCallNotifyEvent::Original(OriginalSyncMessageLikeEvent {
content: CallNotifyEventContent::new(
"call_id".into(),
ApplicationType::Call,
NotifyType::Ring,
Mentions::new(),
),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::YesCallNotify(SyncMessageLikeEvent::Original(_)) =
is_suitable_for_latest_event(&event, None)
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_stickers_are_suitable() {
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::Sticker(
SyncStickerEvent::Original(OriginalSyncMessageLikeEvent {
content: StickerEventContent::new(
"sticker!".to_owned(),
ImageInfo::new(),
owned_mxc_uri!("mxc://example.com/1"),
),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
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 =
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::UnstablePollResponse(
SyncUnstablePollResponseEvent::Original(OriginalSyncMessageLikeEvent {
content: UnstablePollResponseEventContent::new(
vec![String::from("option1")],
owned_event_id!("$1"),
),
event_id: owned_event_id!("$2"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoUnsupportedMessageLikeType
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_redacted_messages_are_suitable() {
// Ruma does not allow constructing UnsignedRoomRedactionEvent instances.
let room_redaction_event = serde_json::from_value(json!({
"content": {},
"event_id": "$redaction",
"sender": "@x:y.za",
"origin_server_ts": 223543,
"unsigned": { "reason": "foo" }
}))
.unwrap();
let event = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Redacted(RedactedSyncMessageLikeEvent {
content: RedactedRoomMessageEventContent::new(),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: RedactedUnsigned::new(room_redaction_event),
}),
));
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 = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomEncrypted(
SyncRoomEncryptedEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomEncryptedEventContent::new(
EncryptedEventScheme::OlmV1Curve25519AesSha2(
OlmV1Curve25519AesSha2Content::new(BTreeMap::new(), "".to_owned()),
),
None,
),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_matches!(
is_suitable_for_latest_event(&event, None),
PossibleLatestEvent::NoEncrypted
);
}
#[cfg(feature = "e2e-encryption")]
#[test]
fn test_state_events_are_unsuitable() {
let event = AnySyncTimelineEvent::State(AnySyncStateEvent::RoomTopic(
SyncRoomTopicEvent::Original(OriginalSyncStateEvent {
content: RoomTopicEventContent::new("".to_owned()),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: StateUnsigned::new(),
state_key: EmptyStateKey,
}),
));
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 = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: event_content,
event_id: owned_event_id!("$2"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(2123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
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 = AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
SyncRoomMessageEvent::Original(OriginalSyncMessageLikeEvent {
content: RoomMessageEventContent::new(MessageType::VerificationRequest(
KeyVerificationRequestEventContent::new(
"body".to_owned(),
vec![],
device_id!("device_id").to_owned(),
user_id!("@user_id:example.com").to_owned(),
),
)),
event_id: owned_event_id!("$1"),
sender: owned_user_id!("@a:b.c"),
origin_server_ts: MilliSecondsSinceUnixEpoch(UInt::new(123).unwrap()),
unsigned: MessageLikeUnsigned::new(),
}),
));
assert_let!(
PossibleLatestEvent::NoUnsupportedMessageLikeType =
is_suitable_for_latest_event(&event, 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,
.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",
}
}
})
);
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
// 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_timestamp_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());
assert_eq!(value.timestamp(), Some(MilliSecondsSinceUnixEpoch(uint!(42))));
}
// The even older format can also be deserialized.
let serialized = json!({
"latest_event": event
#[test]
fn test_event_id_with_none() {
let value = LatestEventValue::None;
assert!(value.event_id().is_none());
}
#[test]
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!(value.event_id(), Some(event_id));
}
#[test]
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"),
))
.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());
assert!(value.event_id().is_none());
}
#[test]
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(),
},
};
assert_eq!(value.event_id(), Some(event_id));
}
#[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(),
});
assert!(value.event_id().is_none());
}
}
+10 -8
View File
@@ -14,7 +14,7 @@
// limitations under the License.
#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(target_family = "wasm", allow(clippy::arc_with_non_send_sync))]
#![warn(missing_docs, missing_debug_implementations)]
@@ -45,6 +45,9 @@ pub mod sync;
mod test_utils;
mod utils;
#[cfg(feature = "experimental-element-recent-emojis")]
pub mod recent_emojis;
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
@@ -57,16 +60,15 @@ pub use once_cell;
pub use room::{
EncryptionState, InviteAcceptanceDetails, PredecessorRoom, Room,
RoomCreateWithCreatorEventContent, RoomDisplayName, RoomHero, RoomInfo, RoomInfoNotableUpdate,
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomState,
RoomStateFilter, SuccessorRoom, apply_redaction,
RoomInfoNotableUpdateReasons, RoomMember, RoomMembersUpdate, RoomMemberships, RoomRecencyStamp,
RoomState, RoomStateFilter, SuccessorRoom, apply_redaction,
};
pub use store::{
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
};
pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
QueueWedgeError, StateChanges, StateStore, StateStoreDataKey, StateStoreDataValue, StoreError,
ThreadSubscriptionCatchupToken,
};
pub use utils::{MinimalRoomMemberEvent, MinimalStateEvent, RawSyncStateEventWithKeys};
#[cfg(test)]
matrix_sdk_test_utils::init_tracing_for_tests!();
@@ -1,4 +1,20 @@
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
// Copyright 2025 Kévin Commaille
//
// 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.
//! Media store and common types for [media content](https://spec.matrix.org/latest/client-server-api/#content-repository).
pub mod store;
use ruma::{
MxcUri, UInt,
@@ -12,26 +12,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! Trait and macro of integration tests for `EventCacheStoreMedia`
//! Trait and macro of integration tests for `MediaStoreInner`
//! implementations.
use ruma::{
events::room::MediaSource,
media::Method,
mxc_uri, owned_mxc_uri,
time::{Duration, SystemTime},
uint,
};
use super::{
EventCacheStoreMedia, MediaRetentionPolicy, media_service::IgnoreMediaRetentionPolicy,
use super::{MediaRetentionPolicy, MediaStoreInner, media_service::IgnoreMediaRetentionPolicy};
use crate::media::{
MediaFormat, MediaRequestParameters, MediaThumbnailSettings, store::MediaStore,
};
use crate::media::{MediaFormat, MediaRequestParameters};
/// [`EventCacheStoreMedia`] integration tests.
/// [`MediaStoreInner`] integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// `event_cache_store_media_integration_tests!` macro.
/// `media_store_inner_integration_tests!` macro.
#[allow(async_fn_in_trait)]
pub trait EventCacheStoreMediaIntegrationTests {
pub trait MediaStoreInnerIntegrationTests {
/// Test media retention policy storage.
async fn test_store_media_retention_policy(&self);
@@ -56,9 +58,9 @@ pub trait EventCacheStoreMediaIntegrationTests {
async fn test_store_last_media_cleanup_time(&self);
}
impl<Store> EventCacheStoreMediaIntegrationTests for Store
impl<Store> MediaStoreInnerIntegrationTests for Store
where
Store: EventCacheStoreMedia + std::fmt::Debug,
Store: MediaStoreInner + std::fmt::Debug,
{
async fn test_store_media_retention_policy(&self) {
let stored = self.media_retention_policy_inner().await.unwrap();
@@ -138,7 +140,7 @@ where
assert!(stored.is_some());
// A cleanup doesn't have any effect.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
assert!(stored.is_some());
@@ -149,7 +151,7 @@ where
let policy = MediaRetentionPolicy::empty().with_max_file_size(Some(100));
// The cleanup removes the average media.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
assert!(stored.is_none());
@@ -217,7 +219,7 @@ where
.with_max_file_size(Some(1000));
// The cleanup removes the average media.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
let stored = self.get_media_content_inner(&request_avg, time).await.unwrap();
assert!(stored.is_none());
@@ -395,7 +397,7 @@ where
// Cleanup removes the oldest content first.
time += Duration::from_secs(1);
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_small_1, time).await.unwrap();
@@ -481,7 +483,7 @@ where
// before.
time += Duration::from_secs(1);
tracing::info!(?self, "before");
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
tracing::info!(?self, "after");
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_small_1, time).await.unwrap();
@@ -602,7 +604,7 @@ where
assert_eq!(time, SystemTime::UNIX_EPOCH + Duration::from_secs(10));
// Cleanup has no effect, nothing has expired.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
@@ -629,7 +631,7 @@ where
time += Duration::from_secs(26);
// Cleanup removes the two oldest media contents.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
@@ -745,7 +747,7 @@ where
// Because the big and average contents are ignored, cleanup has no effect.
time += Duration::from_secs(1);
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
@@ -763,7 +765,7 @@ where
.unwrap();
time += Duration::from_secs(1);
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
@@ -782,7 +784,7 @@ where
.unwrap();
time += Duration::from_secs(1);
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_small, time).await.unwrap();
@@ -892,7 +894,7 @@ where
time += Duration::from_secs(120);
// Cleanup removes all the media contents that are not ignored.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
@@ -922,7 +924,7 @@ where
time += Duration::from_secs(120);
// Cleanup removes the remaining media contents.
self.clean_up_media_cache_inner(policy, time).await.unwrap();
self.clean_inner(policy, time).await.unwrap();
time += Duration::from_secs(1);
let stored = self.get_media_content_inner(&request_1, time).await.unwrap();
@@ -947,21 +949,21 @@ where
// With an empty policy.
let policy = MediaRetentionPolicy::empty();
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
self.clean_inner(policy, new_time).await.unwrap();
let stored = self.last_media_cleanup_time_inner().await.unwrap();
assert_eq!(stored, initial);
// With the default policy.
let policy = MediaRetentionPolicy::default();
self.clean_up_media_cache_inner(policy, new_time).await.unwrap();
self.clean_inner(policy, new_time).await.unwrap();
let stored = self.last_media_cleanup_time_inner().await.unwrap();
assert_eq!(stored, Some(new_time));
}
}
/// Macro building to allow your [`EventCacheStoreMedia`] implementation to run
/// Macro building to allow your [`MediaStoreInner`] implementation to run
/// the entire tests suite locally.
///
/// Can be run with the `with_media_size_tests` argument to include more tests
@@ -969,91 +971,424 @@ where
/// recommended to run those in encrypted stores because the size of the
/// encrypted content may vary compared to what the tests expect.
///
/// You need to provide an `async fn get_event_cache_store() ->
/// event_cache::store::Result<Store>` that provides a fresh event cache store
/// that implements `EventCacheStoreMedia` on the same level you invoke the
/// You need to provide an `async fn get_media_store() ->
/// media::store::Result<Store>` that provides a fresh media store
/// that implements `MediaStoreInner` on the same level you invoke the
/// macro.
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::event_cache::store::{
/// # EventCacheStore,
/// # MemoryStore as MyStore,
/// # Result as EventCacheStoreResult,
/// # use matrix_sdk_base::media::store::{
/// # MediaStore,
/// # MemoryMediaStore as MyStore,
/// # Result as MediaStoreResult,
/// # };
///
/// #[cfg(test)]
/// mod tests {
/// use super::{EventCacheStoreResult, MyStore};
/// use super::{MediaStoreResult, MyStore};
///
/// async fn get_event_cache_store() -> EventCacheStoreResult<MyStore> {
/// async fn get_media_store() -> MediaStoreResult<MyStore> {
/// Ok(MyStore::new())
/// }
///
/// event_cache_store_media_integration_tests!();
/// media_store_inner_integration_tests!();
/// }
/// ```
#[allow(unused_macros, unused_extern_crates)]
#[macro_export]
macro_rules! event_cache_store_media_integration_tests {
macro_rules! media_store_inner_integration_tests {
(with_media_size_tests) => {
mod event_cache_store_media_integration_tests {
$crate::event_cache_store_media_integration_tests!(@inner);
mod media_store_inner_integration_tests {
$crate::media_store_inner_integration_tests!(@inner);
#[async_test]
async fn test_media_max_file_size() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_media_max_file_size().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_media_max_file_size().await;
}
#[async_test]
async fn test_media_max_cache_size() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_media_max_cache_size().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_media_max_cache_size().await;
}
#[async_test]
async fn test_media_ignore_max_size() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_media_ignore_max_size().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_media_ignore_max_size().await;
}
}
};
() => {
mod event_cache_store_media_integration_tests {
$crate::event_cache_store_media_integration_tests!(@inner);
mod media_store_inner_integration_tests {
$crate::media_store_inner_integration_tests!(@inner);
}
};
(@inner) => {
use matrix_sdk_test::async_test;
use $crate::event_cache::store::media::EventCacheStoreMediaIntegrationTests;
use $crate::media::store::MediaStoreInnerIntegrationTests;
use super::get_event_cache_store;
use super::get_media_store;
#[async_test]
async fn test_store_media_retention_policy() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_store_media_retention_policy().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_store_media_retention_policy().await;
}
#[async_test]
async fn test_media_expiry() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_media_expiry().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_media_expiry().await;
}
#[async_test]
async fn test_media_ignore_expiry() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_media_ignore_expiry().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_media_ignore_expiry().await;
}
#[async_test]
async fn test_store_last_media_cleanup_time() {
let event_cache_store_media = get_event_cache_store().await.unwrap();
event_cache_store_media.test_store_last_media_cleanup_time().await;
let media_store_inner = get_media_store().await.unwrap();
media_store_inner.test_store_last_media_cleanup_time().await;
}
};
}
/// [`MediaStore`] integration tests.
///
/// This trait is not meant to be used directly, but will be used with the
/// `media_store_inner_integration_tests!` macro.
#[allow(async_fn_in_trait)]
pub trait MediaStoreIntegrationTests {
/// Test media content storage.
async fn test_media_content(&self);
/// Test replacing a MXID.
async fn test_replace_media_key(&self);
}
impl<Store> MediaStoreIntegrationTests for Store
where
Store: MediaStore + std::fmt::Debug,
{
async fn test_media_content(&self) {
let uri = mxc_uri!("mxc://localhost/media");
let request_file = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let request_thumbnail = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::Thumbnail(MediaThumbnailSettings::with_method(
Method::Crop,
uint!(100),
uint!(100),
)),
};
let other_uri = mxc_uri!("mxc://localhost/media-other");
let request_other_file = MediaRequestParameters {
source: MediaSource::Plain(other_uri.to_owned()),
format: MediaFormat::File,
};
let content: Vec<u8> = "hello".into();
let thumbnail_content: Vec<u8> = "world".into();
let other_content: Vec<u8> = "foo".into();
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"unexpected media found"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"media not found"
);
// Let's add the media.
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.expect("adding media failed");
// Media is present in the cache.
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found though added"
);
assert_eq!(
self.get_media_content_for_uri(uri).await.unwrap().as_ref(),
Some(&content),
"media not found by URI though added"
);
// Let's remove the media.
self.remove_media_content(&request_file).await.expect("removing media failed");
// Media isn't present in the cache.
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media still there after removing"
);
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
"media still found by URI after removing"
);
// Let's add the media again.
self.add_media_content(&request_file, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.expect("adding media again failed");
assert_eq!(
self.get_media_content(&request_file).await.unwrap().as_ref(),
Some(&content),
"media not found after adding again"
);
// Let's add the thumbnail media.
self.add_media_content(
&request_thumbnail,
thumbnail_content.clone(),
IgnoreMediaRetentionPolicy::No,
)
.await
.expect("adding thumbnail failed");
// Media's thumbnail is present.
assert_eq!(
self.get_media_content(&request_thumbnail).await.unwrap().as_ref(),
Some(&thumbnail_content),
"thumbnail not found"
);
// We get a file with the URI, we don't know which one.
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_some(),
"media not found by URI though two where added"
);
// Let's add another media with a different URI.
self.add_media_content(
&request_other_file,
other_content.clone(),
IgnoreMediaRetentionPolicy::No,
)
.await
.expect("adding other media failed");
// Other file is present.
assert_eq!(
self.get_media_content(&request_other_file).await.unwrap().as_ref(),
Some(&other_content),
"other file not found"
);
assert_eq!(
self.get_media_content_for_uri(other_uri).await.unwrap().as_ref(),
Some(&other_content),
"other file not found by URI"
);
// Let's remove media based on URI.
self.remove_media_content_for_uri(uri).await.expect("removing all media for uri failed");
assert!(
self.get_media_content(&request_file).await.unwrap().is_none(),
"media wasn't removed"
);
assert!(
self.get_media_content(&request_thumbnail).await.unwrap().is_none(),
"thumbnail wasn't removed"
);
assert!(
self.get_media_content(&request_other_file).await.unwrap().is_some(),
"other media was removed"
);
assert!(
self.get_media_content_for_uri(uri).await.unwrap().is_none(),
"media found by URI wasn't removed"
);
assert!(
self.get_media_content_for_uri(other_uri).await.unwrap().is_some(),
"other media found by URI was removed"
);
}
async fn test_replace_media_key(&self) {
let uri = mxc_uri!("mxc://sendqueue.local/tr4n-s4ct-10n1-d");
let req = MediaRequestParameters {
source: MediaSource::Plain(uri.to_owned()),
format: MediaFormat::File,
};
let content = "hello".as_bytes().to_owned();
// Media isn't present in the cache.
assert!(self.get_media_content(&req).await.unwrap().is_none(), "unexpected media found");
// Add the media.
self.add_media_content(&req, content.clone(), IgnoreMediaRetentionPolicy::No)
.await
.expect("adding media failed");
// Sanity-check: media is found after adding it.
assert_eq!(self.get_media_content(&req).await.unwrap().unwrap(), b"hello");
// Replacing a media request works.
let new_uri = mxc_uri!("mxc://matrix.org/tr4n-s4ct-10n1-d");
let new_req = MediaRequestParameters {
source: MediaSource::Plain(new_uri.to_owned()),
format: MediaFormat::File,
};
self.replace_media_key(&req, &new_req)
.await
.expect("replacing the media request key failed");
// Finding with the previous request doesn't work anymore.
assert!(
self.get_media_content(&req).await.unwrap().is_none(),
"unexpected media found with the old key"
);
// Finding with the new request does work.
assert_eq!(self.get_media_content(&new_req).await.unwrap().unwrap(), b"hello");
}
}
/// Macro building to allow your [`MediaStore`] implementation to run
/// the entire tests suite locally.
///
/// You need to provide an `async fn get_media_store() ->
/// media::store::Result<Store>` that provides a fresh media store
/// that implements `MediaStoreInner` on the same level you invoke the
/// macro.
///
/// ## Usage Example:
/// ```no_run
/// # use matrix_sdk_base::media::store::{
/// # MediaStore,
/// # MemoryMediaStore as MyStore,
/// # Result as MediaStoreResult,
/// # };
///
/// #[cfg(test)]
/// mod tests {
/// use super::{MediaStoreResult, MyStore};
///
/// async fn get_media_store() -> MediaStoreResult<MyStore> {
/// Ok(MyStore::new())
/// }
///
/// media_store_integration_tests!();
/// }
/// ```
#[allow(unused_macros, unused_extern_crates)]
#[macro_export]
macro_rules! media_store_integration_tests {
() => {
mod media_store_integration_tests {
use matrix_sdk_test::async_test;
use $crate::media::store::integration_tests::MediaStoreIntegrationTests;
use super::get_media_store;
#[async_test]
async fn test_media_content() {
let media_store = get_media_store().await.unwrap();
media_store.test_media_content().await;
}
#[async_test]
async fn test_replace_media_key() {
let media_store = get_media_store().await.unwrap();
media_store.test_replace_media_key().await;
}
}
};
}
/// Macro generating tests for the media store, related to time (mostly
/// for the cross-process lock).
#[allow(unused_macros)]
#[macro_export]
macro_rules! media_store_integration_tests_time {
() => {
mod media_store_integration_tests_time {
use std::time::Duration;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use gloo_timers::future::sleep;
use matrix_sdk_test::async_test;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use tokio::time::sleep;
use $crate::media::store::MediaStore;
use super::get_media_store;
#[async_test]
async fn test_lease_locks() {
let store = get_media_store().await.unwrap();
let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert_eq!(acquired0, Some(1)); // first lock generation
// Should extend the lease automatically (same holder).
let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert_eq!(acquired2, Some(1)); // same lock generation
// Should extend the lease automatically (same holder + time is ok).
let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert_eq!(acquired3, Some(1)); // same lock generation
// Another attempt at taking the lock should fail, because it's taken.
let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired4.is_none()); // not acquired
// Even if we insist.
let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired5.is_none()); // not acquired
// That's a nice test we got here, go take a little nap.
sleep(Duration::from_millis(50)).await;
// Still too early.
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(acquired55.is_none()); // not acquired
// Ok you can take another nap then.
sleep(Duration::from_millis(250)).await;
// At some point, we do get the lock.
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
assert_eq!(acquired6, Some(2)); // new lock generation!
sleep(Duration::from_millis(1)).await;
// The other gets it almost immediately too.
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert_eq!(acquired7, Some(3)); // new lock generation!
sleep(Duration::from_millis(1)).await;
// But when we take a longer lease…
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert_eq!(acquired8, Some(4)); // new lock generation!
// It blocks the other user.
let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap();
assert!(acquired9.is_none()); // not acquired
// We can hold onto our lease.
let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert_eq!(acquired10, Some(4)); // same lock generation
}
}
};
}
@@ -17,19 +17,22 @@
//! indefinitely.
//!
//! To proceed to a cleanup, first set the [`MediaRetentionPolicy`] to use with
//! [`EventCacheStore::set_media_retention_policy()`]. Then call
//! [`EventCacheStore::clean_up_media_cache()`].
//! [`MediaStore::set_media_retention_policy()`]. Then call
//! [`MediaStore::clean()`].
//!
//! In the future, other settings will allow to run automatic periodic cleanup
//! jobs.
//!
//! [`EventCacheStore::set_media_retention_policy()`]: crate::event_cache::store::EventCacheStore::set_media_retention_policy
//! [`EventCacheStore::clean_up_media_cache()`]: crate::event_cache::store::EventCacheStore::clean_up_media_cache
//! [`MediaStore::set_media_retention_policy()`]: crate::media::store::MediaStore::set_media_retention_policy
//! [`MediaStore::clean()`]: crate::media::store::MediaStore::clean
use ruma::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
/// The retention policy for media content used by the [`EventCacheStore`].
#[cfg(doc)]
use crate::media::store::MediaStore;
/// The retention policy for media content used by the [`MediaStore`].
///
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -12,11 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt, sync::Arc};
use std::sync::Arc;
use async_trait::async_trait;
use matrix_sdk_common::{
AsyncTraitDeps, SendOutsideWasm, SyncOutsideWasm,
SendOutsideWasm, SyncOutsideWasm,
executor::{JoinHandle, spawn},
locks::Mutex,
};
@@ -24,13 +23,13 @@ use ruma::{MxcUri, time::SystemTime};
use tokio::sync::Mutex as AsyncMutex;
use tracing::error;
use super::MediaRetentionPolicy;
use crate::{event_cache::store::EventCacheStoreError, media::MediaRequestParameters};
use super::{MediaRetentionPolicy, MediaStoreInner};
use crate::media::MediaRequestParameters;
/// API for implementors of [`EventCacheStore`] to manage their media through
/// their implementation of [`EventCacheStoreMedia`].
/// API for implementors of [`MediaStore`] to manage their media through
/// their implementation of [`MediaStoreInner`].
///
/// [`EventCacheStore`]: crate::event_cache::store::EventCacheStore
/// [`MediaStore`]: crate::media::store::MediaStore
#[derive(Debug)]
pub struct MediaService<Time: TimeProvider = DefaultTimeProvider> {
inner: Arc<MediaServiceInner<Time>>,
@@ -122,10 +121,10 @@ where
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
/// * `store` - The `MediaStoreInner`.
///
/// * `policy` - The `MediaRetentionPolicy` to use.
pub async fn set_media_retention_policy<Store: EventCacheStoreMedia + 'static>(
pub async fn set_media_retention_policy<Store: MediaStoreInner + 'static>(
&self,
store: &Store,
policy: MediaRetentionPolicy,
@@ -148,7 +147,7 @@ where
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
/// * `store` - The `MediaStoreInner`.
///
/// * `request` - The `MediaRequestParameters` of the file.
///
@@ -156,7 +155,7 @@ where
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
pub async fn add_media_content<Store: EventCacheStoreMedia + 'static>(
pub async fn add_media_content<Store: MediaStoreInner + 'static>(
&self,
store: &Store,
request: &MediaRequestParameters,
@@ -189,13 +188,13 @@ where
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
/// * `store` - The `MediaStoreInner`.
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
pub async fn set_ignore_media_retention_policy<Store: EventCacheStoreMedia>(
pub async fn set_ignore_media_retention_policy<Store: MediaStoreInner>(
&self,
store: &Store,
request: &MediaRequestParameters,
@@ -208,10 +207,10 @@ where
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
/// * `store` - The `MediaStoreInner`.
///
/// * `request` - The `MediaRequestParameters` of the file.
pub async fn get_media_content<Store: EventCacheStoreMedia + 'static>(
pub async fn get_media_content<Store: MediaStoreInner + 'static>(
&self,
store: &Store,
request: &MediaRequestParameters,
@@ -229,10 +228,10 @@ where
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
/// * `store` - The `MediaStoreInner`.
///
/// * `uri` - The `MxcUri` of the media file.
pub async fn get_media_content_for_uri<Store: EventCacheStoreMedia + 'static>(
pub async fn get_media_content_for_uri<Store: MediaStoreInner + 'static>(
&self,
store: &Store,
uri: &MxcUri,
@@ -251,15 +250,12 @@ where
///
/// # Arguments
///
/// * `store` - The `EventCacheStoreMedia`.
pub async fn clean_up_media_cache<Store: EventCacheStoreMedia>(
&self,
store: &Store,
) -> Result<(), Store::Error> {
self.clean_up_media_cache_inner(store, self.now()).await
/// * `store` - The `MediaStoreInner`.
pub async fn clean<Store: MediaStoreInner>(&self, store: &Store) -> Result<(), Store::Error> {
self.clean_inner(store, self.now()).await
}
async fn clean_up_media_cache_inner<Store: EventCacheStoreMedia>(
async fn clean_inner<Store: MediaStoreInner>(
&self,
store: &Store,
current_time: SystemTime,
@@ -276,7 +272,7 @@ where
return Ok(());
}
store.clean_up_media_cache_inner(policy, current_time).await?;
store.clean_inner(policy, current_time).await?;
*self.inner.last_media_cleanup_time.lock() = Some(current_time);
@@ -290,7 +286,7 @@ where
/// * The media retention policy's `cleanup_frequency` is set and enough
/// time has passed since the last cleanup.
/// * No other cleanup is running,
fn maybe_spawn_automatic_media_cache_cleanup<Store: EventCacheStoreMedia + 'static>(
fn maybe_spawn_automatic_media_cache_cleanup<Store: MediaStoreInner + 'static>(
&self,
store: &Store,
current_time: SystemTime,
@@ -320,7 +316,7 @@ where
let store = store.clone();
let handle = spawn(async move {
if let Err(error) = this.clean_up_media_cache_inner(&store, current_time).await {
if let Err(error) = this.clean_inner(&store, current_time).await {
error!("Failed to run automatic media cache cleanup: {error}");
}
});
@@ -349,132 +345,6 @@ where
}
}
/// An abstract trait that can be used to implement different store backends
/// for the media cache of the SDK.
///
/// The main purposes of this trait are to be able to centralize where we handle
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
/// simplify the implementation of tests by being able to have complete control
/// over the `SystemTime`s provided to the store.
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
pub trait EventCacheStoreMedia: AsyncTraitDeps + Clone {
/// The error type used by this media cache store.
type Error: fmt::Debug + fmt::Display + Into<EventCacheStoreError>;
/// The persisted media retention policy in the media cache.
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
/// Persist the media retention policy in the media cache.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` to persist.
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Add a media file's content in the media cache.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `content` - The content of the file.
///
/// * `current_time` - The current time, to set the last access time of the
/// media.
///
/// * `policy` - The media retention policy, to check whether the media is
/// too big to be cached.
///
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
/// for this media. This setting should be persisted alongside the media
/// and taken into account whenever the policy is used.
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
current_time: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// If the media of the given request is not found, this should be a noop.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media cache.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `current_time` - The current time, to update the last access time of
/// the media.
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
///
/// * `current_time` - The current time, to update the last access time of
/// the media.
async fn get_media_content_for_uri_inner(
&self,
uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Clean up the media cache with the given policy.
///
/// For the integration tests, it is expected that content that does not
/// pass the last access expiry and max file size criteria will be
/// removed first. After that, the remaining cache size should be
/// computed to compare against the max cache size criteria.
///
/// # Arguments
///
/// * `policy` - The media retention policy to use for the cleanup. The
/// `cleanup_frequency` will be ignored.
///
/// * `current_time` - The current time, to be used to check for expired
/// content and to be stored as the time of the last media cache cleanup.
async fn clean_up_media_cache_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error>;
/// The time of the last media cache cleanup.
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
}
/// Whether the [`MediaRetentionPolicy`] should be ignored for the current
/// content.
///
@@ -544,18 +414,18 @@ mod tests {
time::{Duration, SystemTime},
};
use super::{EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaService, TimeProvider};
use crate::{
event_cache::store::{EventCacheStoreError, media::MediaRetentionPolicy},
media::{MediaFormat, MediaRequestParameters, UniqueKey},
use super::{
IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStoreInner,
TimeProvider,
};
use crate::media::{MediaFormat, MediaRequestParameters, UniqueKey, store::MediaStoreError};
#[derive(Debug, Default, Clone)]
struct MockEventCacheStoreMedia {
inner: Arc<Mutex<MockEventCacheStoreMediaInner>>,
struct MockMediaStoreInner {
inner: Arc<Mutex<MockMediaStoreInnerInner>>,
}
impl MockEventCacheStoreMedia {
impl MockMediaStoreInner {
/// Whether the store was accessed.
fn accessed(&self) -> bool {
self.inner.lock().accessed
@@ -570,7 +440,7 @@ mod tests {
///
/// Should be called for every access to the inner store as it also sets
/// the `accessed` boolean.
fn inner(&self) -> MutexGuard<'_, MockEventCacheStoreMediaInner> {
fn inner(&self) -> MutexGuard<'_, MockMediaStoreInnerInner> {
let mut inner = self.inner.lock();
inner.accessed = true;
inner
@@ -578,7 +448,7 @@ mod tests {
}
#[derive(Debug, Default)]
struct MockEventCacheStoreMediaInner {
struct MockMediaStoreInnerInner {
/// Whether this store was accessed.
///
/// Must be set to `true` for any operation that unlocks the store.
@@ -614,26 +484,26 @@ mod tests {
}
#[derive(Debug)]
struct MockEventCacheStoreMediaError;
struct MockMediaStoreInnerError;
impl fmt::Display for MockEventCacheStoreMediaError {
impl fmt::Display for MockMediaStoreInnerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MockEventCacheStoreMediaError")
write!(f, "MockMediaStoreInnerError")
}
}
impl std::error::Error for MockEventCacheStoreMediaError {}
impl std::error::Error for MockMediaStoreInnerError {}
impl From<MockEventCacheStoreMediaError> for EventCacheStoreError {
fn from(value: MockEventCacheStoreMediaError) -> Self {
impl From<MockMediaStoreInnerError> for MediaStoreError {
fn from(value: MockMediaStoreInnerError) -> Self {
Self::backend(value)
}
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl EventCacheStoreMedia for MockEventCacheStoreMedia {
type Error = MockEventCacheStoreMediaError;
impl MediaStoreInner for MockMediaStoreInner {
type Error = MockMediaStoreInnerError;
async fn media_retention_policy_inner(
&self,
@@ -736,7 +606,7 @@ mod tests {
Ok(Some(media_content.content.clone()))
}
async fn clean_up_media_cache_inner(
async fn clean_inner(
&self,
_policy: MediaRetentionPolicy,
current_time: SystemTime,
@@ -787,7 +657,7 @@ mod tests {
let now = SystemTime::UNIX_EPOCH;
let store = MockEventCacheStoreMedia::default();
let store = MockMediaStoreInner::default();
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// By default an empty policy is used.
@@ -849,7 +719,7 @@ mod tests {
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
store.reset_accessed();
service.clean_up_media_cache(&store).await.unwrap();
service.clean(&store).await.unwrap();
assert!(!store.accessed());
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), None);
}
@@ -877,7 +747,7 @@ mod tests {
let now = SystemTime::UNIX_EPOCH;
let store = MockEventCacheStoreMedia::default();
let store = MockMediaStoreInner::default();
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// Check that restoring the policy works.
@@ -1011,7 +881,7 @@ mod tests {
service.inner.time_provider.set_now(now);
store.reset_accessed();
service.clean_up_media_cache(&store).await.unwrap();
service.clean(&store).await.unwrap();
assert!(store.accessed());
assert_eq!(store.last_media_cleanup_time_inner().await.unwrap(), Some(now));
}
@@ -1034,7 +904,7 @@ mod tests {
let now = SystemTime::UNIX_EPOCH;
let store = MockEventCacheStoreMedia::default();
let store = MockMediaStoreInner::default();
let service = MediaService::with_time_provider(MockTimeProvider::new(now));
// Set an empty policy.
@@ -0,0 +1,451 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::HashMap,
num::NonZeroUsize,
sync::{Arc, RwLock as StdRwLock},
};
use async_trait::async_trait;
use matrix_sdk_common::{
cross_process_lock::{
CrossProcessLockGeneration,
memory_store_helper::{Lease, try_take_leased_lock},
},
ring_buffer::RingBuffer,
};
use ruma::{MxcUri, OwnedMxcUri, time::SystemTime};
use super::Result;
use crate::media::{
MediaRequestParameters, UniqueKey as _,
store::{
IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore,
MediaStoreError, MediaStoreInner,
},
};
/// In-memory, non-persistent implementation of the `MediaStore`.
///
/// Default if no other is configured at startup.
#[derive(Debug, Clone)]
pub struct MemoryMediaStore {
inner: Arc<StdRwLock<MemoryMediaStoreInner>>,
media_service: MediaService,
}
#[derive(Debug)]
struct MemoryMediaStoreInner {
media: RingBuffer<MediaContent>,
leases: HashMap<String, Lease>,
media_retention_policy: Option<MediaRetentionPolicy>,
last_media_cleanup_time: SystemTime,
}
/// A media content in the `MemoryStore`.
#[derive(Debug)]
struct MediaContent {
/// The URI of the content.
uri: OwnedMxcUri,
/// The unique key of the content.
key: String,
/// The bytes of the content.
data: Vec<u8>,
/// Whether we should ignore the [`MediaRetentionPolicy`] for this content.
ignore_policy: bool,
/// The time of the last access of the content.
last_access: SystemTime,
}
const NUMBER_OF_MEDIAS: NonZeroUsize = NonZeroUsize::new(20).unwrap();
impl Default for MemoryMediaStore {
fn default() -> Self {
// Given that the store is empty, we won't need to clean it up right away.
let last_media_cleanup_time = SystemTime::now();
let media_service = MediaService::new();
media_service.restore(None, Some(last_media_cleanup_time));
Self {
inner: Arc::new(StdRwLock::new(MemoryMediaStoreInner {
media: RingBuffer::new(NUMBER_OF_MEDIAS),
leases: Default::default(),
media_retention_policy: None,
last_media_cleanup_time,
})),
media_service,
}
}
}
impl MemoryMediaStore {
/// Create a new empty MemoryMediaStore
pub fn new() -> Self {
Self::default()
}
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl MediaStore for MemoryMediaStore {
type Error = MediaStoreError;
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
let mut inner = self.inner.write().unwrap();
Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder))
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.add_media_content(self, request, data, ignore_policy).await
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
let expected_key = from.unique_key();
let mut inner = self.inner.write().unwrap();
if let Some(media_content) =
inner.media.iter_mut().find(|media_content| media_content.key == expected_key)
{
media_content.uri = to.uri().to_owned();
media_content.key = to.unique_key();
}
Ok(())
}
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.media_service.get_media_content(self, request).await
}
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error> {
let expected_key = request.unique_key();
let mut inner = self.inner.write().unwrap();
let Some(index) =
inner.media.iter().position(|media_content| media_content.key == expected_key)
else {
return Ok(());
};
inner.media.remove(index);
Ok(())
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.media_service.get_media_content_for_uri(self, uri).await
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
let positions = inner
.media
.iter()
.enumerate()
.filter_map(|(position, media_content)| (media_content.uri == uri).then_some(position))
.collect::<Vec<_>>();
// Iterate in reverse-order so that positions stay valid after first removals.
for position in positions.into_iter().rev() {
inner.media.remove(position);
}
Ok(())
}
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.set_media_retention_policy(self, policy).await
}
fn media_retention_policy(&self) -> MediaRetentionPolicy {
self.media_service.media_retention_policy()
}
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await
}
async fn clean(&self) -> Result<(), Self::Error> {
self.media_service.clean(self).await
}
async fn optimize(&self) -> Result<(), Self::Error> {
Ok(())
}
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
Ok(None)
}
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl MediaStoreInner for MemoryMediaStore {
type Error = MediaStoreError;
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error> {
Ok(self.inner.read().unwrap().media_retention_policy)
}
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.inner.write().unwrap().media_retention_policy = Some(policy);
Ok(())
}
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
data: Vec<u8>,
last_access: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
// Avoid duplication. Let's try to remove it first.
self.remove_media_content(request).await?;
let ignore_policy = ignore_policy.is_yes();
if !ignore_policy && policy.exceeds_max_file_size(data.len() as u64) {
// Do not store it.
return Ok(());
}
// Now, let's add it.
let mut inner = self.inner.write().unwrap();
inner.media.push(MediaContent {
uri: request.uri().to_owned(),
key: request.unique_key(),
data,
ignore_policy,
last_access,
});
Ok(())
}
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
let mut inner = self.inner.write().unwrap();
let expected_key = request.unique_key();
if let Some(media_content) = inner.media.iter_mut().find(|media| media.key == expected_key)
{
media_content.ignore_policy = ignore_policy.is_yes();
}
Ok(())
}
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner.write().unwrap();
let expected_key = request.unique_key();
// First get the content out of the buffer, we are going to put it back at the
// end.
let Some(index) = inner.media.iter().position(|media| media.key == expected_key) else {
return Ok(None);
};
let Some(mut content) = inner.media.remove(index) else {
return Ok(None);
};
// Clone the data.
let data = content.data.clone();
// Update the last access time.
content.last_access = current_time;
// Put it back in the buffer.
inner.media.push(content);
Ok(Some(data))
}
async fn get_media_content_for_uri_inner(
&self,
expected_uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error> {
let mut inner = self.inner.write().unwrap();
// First get the content out of the buffer, we are going to put it back at the
// end.
let Some(index) = inner.media.iter().position(|media| media.uri == expected_uri) else {
return Ok(None);
};
let Some(mut content) = inner.media.remove(index) else {
return Ok(None);
};
// Clone the data.
let data = content.data.clone();
// Update the last access time.
content.last_access = current_time;
// Put it back in the buffer.
inner.media.push(content);
Ok(Some(data))
}
async fn clean_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error> {
if !policy.has_limitations() {
// We can safely skip all the checks.
return Ok(());
}
let mut inner = self.inner.write().unwrap();
// First, check media content that exceed the max filesize.
if policy.computed_max_file_size().is_some() {
inner.media.retain(|content| {
content.ignore_policy || !policy.exceeds_max_file_size(content.data.len() as u64)
});
}
// Then, clean up expired media content.
if policy.last_access_expiry.is_some() {
inner.media.retain(|content| {
content.ignore_policy
|| !policy.has_content_expired(current_time, content.last_access)
});
}
// Finally, if the cache size is too big, remove old items until it fits.
if let Some(max_cache_size) = policy.max_cache_size {
// Reverse the iterator because in case the cache size is overflowing, we want
// to count the number of old items to remove. Items are sorted by last access
// and old items are at the start.
let (_, items_to_remove) = inner.media.iter().enumerate().rev().fold(
(0u64, Vec::with_capacity(NUMBER_OF_MEDIAS.into())),
|(mut cache_size, mut items_to_remove), (index, content)| {
if content.ignore_policy {
// Do not count it.
return (cache_size, items_to_remove);
}
let remove_item = if items_to_remove.is_empty() {
// We have not reached the max cache size yet.
if let Some(sum) = cache_size.checked_add(content.data.len() as u64) {
cache_size = sum;
// Start removing items if we have exceeded the max cache size.
cache_size > max_cache_size
} else {
// The cache size is overflowing, remove the remaining items, since the
// max cache size cannot be bigger than
// usize::MAX.
true
}
} else {
// We have reached the max cache size already, just remove it.
true
};
if remove_item {
items_to_remove.push(index);
}
(cache_size, items_to_remove)
},
);
// The indexes are already in reverse order so we can just iterate in that order
// to remove them starting by the end.
for index in items_to_remove {
inner.media.remove(index);
}
}
inner.last_media_cleanup_time = current_time;
Ok(())
}
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error> {
Ok(Some(self.inner.read().unwrap().last_media_cleanup_time))
}
}
#[cfg(test)]
mod tests {
use super::{MemoryMediaStore, Result};
use crate::{
media_store_inner_integration_tests, media_store_integration_tests,
media_store_integration_tests_time,
};
async fn get_media_store() -> Result<MemoryMediaStore> {
Ok(MemoryMediaStore::new())
}
media_store_inner_integration_tests!();
media_store_integration_tests!();
media_store_integration_tests_time!();
}
@@ -0,0 +1,198 @@
// Copyright 2025 Kévin Commaille
//
// 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.
//! The media store holds downloaded media when the cache was
//! activated to save bandwidth at the cost of increased storage space usage.
//!
//! Implementing the `MediaStore` trait, you can plug any storage backend
//! into the media store for the actual storage. By default this brings an
//! in-memory store.
mod media_retention_policy;
mod media_service;
mod memory_store;
mod traits;
#[cfg(any(test, feature = "testing"))]
#[macro_use]
pub mod integration_tests;
#[cfg(not(tarpaulin_include))]
use std::fmt;
use std::{ops::Deref, sync::Arc};
use matrix_sdk_common::cross_process_lock::{
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
CrossProcessLockState, TryLock,
};
use matrix_sdk_store_encryption::Error as StoreEncryptionError;
pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner};
#[cfg(any(test, feature = "testing"))]
pub use self::integration_tests::{MediaStoreInnerIntegrationTests, MediaStoreIntegrationTests};
pub use self::{
media_retention_policy::MediaRetentionPolicy,
media_service::{IgnoreMediaRetentionPolicy, MediaService},
memory_store::MemoryMediaStore,
};
/// Media store specific error type.
#[derive(Debug, thiserror::Error)]
pub enum MediaStoreError {
/// An error happened in the underlying database backend.
#[error(transparent)]
Backend(Box<dyn std::error::Error + Send + Sync>),
/// The store failed to encrypt or decrypt some data.
#[error("Error encrypting or decrypting data from the media store: {0}")]
Encryption(#[from] StoreEncryptionError),
/// The store contains invalid data.
#[error("The store contains invalid data: {details}")]
InvalidData {
/// Details why the data contained in the store was invalid.
details: String,
},
/// The store failed to serialize or deserialize some data.
#[error("Error serializing or deserializing data from the media store: {0}")]
Serialization(#[from] serde_json::Error),
}
impl MediaStoreError {
/// Create a new [`Backend`][Self::Backend] error.
///
/// Shorthand for `MediaStoreError::Backend(Box::new(error))`.
#[inline]
pub fn backend<E>(error: E) -> Self
where
E: std::error::Error + Send + Sync + 'static,
{
Self::Backend(Box::new(error))
}
}
impl From<MediaStoreError> for CrossProcessLockError {
fn from(value: MediaStoreError) -> Self {
Self::TryLock(Box::new(value))
}
}
/// An `MediaStore` specific result type.
pub type Result<T, E = MediaStoreError> = std::result::Result<T, E>;
/// The high-level public type to represent an `MediaStore` lock.
#[derive(Clone)]
pub struct MediaStoreLock {
/// The inner cross process lock that is used to lock the `MediaStore`.
cross_process_lock: Arc<CrossProcessLock<LockableMediaStore>>,
/// The store itself.
///
/// That's the only place where the store exists.
store: Arc<DynMediaStore>,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for MediaStoreLock {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("MediaStoreLock").finish_non_exhaustive()
}
}
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
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,
}
}
/// Acquire a spin lock (see [`CrossProcessLock::spin_lock`]).
pub async fn lock(&self) -> Result<MediaStoreLockGuard<'_>, CrossProcessLockError> {
let cross_process_lock_guard = match self.cross_process_lock.spin_lock(None).await?? {
// The lock is clean: no other hold acquired it, all good!
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.
CrossProcessLockState::Dirty(guard) => {
guard.clear_dirty();
guard
}
};
Ok(MediaStoreLockGuard { cross_process_lock_guard, store: self.store.deref() })
}
}
/// An RAII implementation of a “scoped lock” of an [`MediaStoreLock`].
/// When this structure is dropped (falls out of scope), the lock will be
/// unlocked.
pub struct MediaStoreLockGuard<'a> {
/// The cross process lock guard.
#[allow(unused)]
cross_process_lock_guard: CrossProcessLockGuard,
/// A reference to the store.
store: &'a DynMediaStore,
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for MediaStoreLockGuard<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_struct("MediaStoreLockGuard").finish_non_exhaustive()
}
}
impl Deref for MediaStoreLockGuard<'_> {
type Target = DynMediaStore;
fn deref(&self) -> &Self::Target {
self.store
}
}
/// A type that wraps the [`MediaStore`] but implements [`TryLock`] to
/// make it usable inside the cross process lock.
#[derive(Clone, Debug)]
struct LockableMediaStore(Arc<DynMediaStore>);
impl TryLock for LockableMediaStore {
type LockError = MediaStoreError;
async fn try_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> std::result::Result<Option<CrossProcessLockGeneration>, Self::LockError> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await
}
}
@@ -0,0 +1,446 @@
// Copyright 2025 Kévin Commaille
//
// 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.
//! Types and traits regarding media caching of the media store.
use std::{fmt, sync::Arc};
use async_trait::async_trait;
use matrix_sdk_common::{AsyncTraitDeps, cross_process_lock::CrossProcessLockGeneration};
use ruma::{MxcUri, time::SystemTime};
#[cfg(doc)]
use crate::media::store::MediaService;
use crate::media::{
MediaRequestParameters,
store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaStoreError},
};
/// An abstract trait that can be used to implement different store backends
/// for the media of the SDK.
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
pub trait MediaStore: AsyncTraitDeps {
/// The error type used by this media store.
type Error: fmt::Debug + Into<MediaStoreError>;
/// Try to take a lock using the given store.
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<Option<CrossProcessLockGeneration>, Self::Error>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Replaces the given media's content key with another one.
///
/// This should be used whenever a temporary (local) MXID has been used, and
/// it must now be replaced with its actual remote counterpart (after
/// uploading some content, or creating an empty MXC URI).
///
/// ⚠ No check is performed to ensure that the media formats are consistent,
/// i.e. it's possible to update with a thumbnail key a media that was
/// keyed as a file before. The caller is responsible of ensuring that
/// the replacement makes sense, according to their use case.
///
/// This should not raise an error when the `from` parameter points to an
/// unknown media, and it should silently continue in this case.
///
/// # Arguments
///
/// * `from` - The previous `MediaRequest` of the file.
///
/// * `to` - The new `MediaRequest` of the file.
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Remove a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// In theory, there could be several files stored using the same URI and a
/// different `MediaFormat`. This API is meant to be used with a media file
/// that has only been stored with a single format.
///
/// If there are several media files for a given URI in different formats,
/// this API will only return one of them. Which one is left as an
/// implementation detail.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
async fn get_media_content_for_uri(&self, uri: &MxcUri)
-> Result<Option<Vec<u8>>, Self::Error>;
/// Remove all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// This should not raise an error when the `uri` parameter points to an
/// unknown media, and it should return an Ok result in this case.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error>;
/// Set the `MediaRetentionPolicy` to use for deciding whether to store or
/// keep media content.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` to use.
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Get the current `MediaRetentionPolicy`.
fn media_retention_policy(&self) -> MediaRetentionPolicy;
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Clean up the media cache with the current `MediaRetentionPolicy`.
///
/// If there is already an ongoing cleanup, this is a noop.
async fn clean(&self) -> Result<(), Self::Error>;
/// Perform database optimizations if any are available, i.e. vacuuming in
/// SQLite.
///
/// **Warning:** this was added to check if SQLite fragmentation was the
/// source of performance issues, **DO NOT use in production**.
#[doc(hidden)]
async fn optimize(&self) -> Result<(), Self::Error>;
/// Returns the size of the store in bytes, if known.
async fn get_size(&self) -> Result<Option<usize>, Self::Error>;
}
/// An abstract trait that can be used to implement different store backends
/// for the media cache of the SDK.
///
/// The main purposes of this trait are to be able to centralize where we handle
/// [`MediaRetentionPolicy`] by wrapping this in a [`MediaService`], and to
/// simplify the implementation of tests by being able to have complete control
/// over the `SystemTime`s provided to the store.
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
pub trait MediaStoreInner: AsyncTraitDeps + Clone {
/// The error type used by this media cache store.
type Error: fmt::Debug + fmt::Display + Into<MediaStoreError>;
/// The persisted media retention policy in the media cache.
async fn media_retention_policy_inner(
&self,
) -> Result<Option<MediaRetentionPolicy>, Self::Error>;
/// Persist the media retention policy in the media cache.
///
/// # Arguments
///
/// * `policy` - The `MediaRetentionPolicy` to persist.
async fn set_media_retention_policy_inner(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Add a media file's content in the media cache.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `content` - The content of the file.
///
/// * `current_time` - The current time, to set the last access time of the
/// media.
///
/// * `policy` - The media retention policy, to check whether the media is
/// too big to be cached.
///
/// * `ignore_policy` - Whether the `MediaRetentionPolicy` should be ignored
/// for this media. This setting should be persisted alongside the media
/// and taken into account whenever the policy is used.
async fn add_media_content_inner(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
current_time: SystemTime,
policy: MediaRetentionPolicy,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Set whether the current [`MediaRetentionPolicy`] should be ignored for
/// the media.
///
/// If the media of the given request is not found, this should be a noop.
///
/// The change will be taken into account in the next cleanup.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `ignore_policy` - Whether the current `MediaRetentionPolicy` should be
/// ignored.
async fn set_ignore_media_retention_policy_inner(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error>;
/// Get a media file's content out of the media cache.
///
/// # Arguments
///
/// * `request` - The `MediaRequestParameters` of the file.
///
/// * `current_time` - The current time, to update the last access time of
/// the media.
async fn get_media_content_inner(
&self,
request: &MediaRequestParameters,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Get a media file's content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media file.
///
/// * `current_time` - The current time, to update the last access time of
/// the media.
async fn get_media_content_for_uri_inner(
&self,
uri: &MxcUri,
current_time: SystemTime,
) -> Result<Option<Vec<u8>>, Self::Error>;
/// Clean up the media cache with the given policy.
///
/// For the integration tests, it is expected that content that does not
/// pass the last access expiry and max file size criteria will be
/// removed first. After that, the remaining cache size should be
/// computed to compare against the max cache size criteria.
///
/// # Arguments
///
/// * `policy` - The media retention policy to use for the cleanup. The
/// `cleanup_frequency` will be ignored.
///
/// * `current_time` - The current time, to be used to check for expired
/// content and to be stored as the time of the last media cache cleanup.
async fn clean_inner(
&self,
policy: MediaRetentionPolicy,
current_time: SystemTime,
) -> Result<(), Self::Error>;
/// The time of the last media cache cleanup.
async fn last_media_cleanup_time_inner(&self) -> Result<Option<SystemTime>, Self::Error>;
}
#[repr(transparent)]
struct EraseMediaStoreError<T>(T);
#[cfg(not(tarpaulin_include))]
impl<T: fmt::Debug> fmt::Debug for EraseMediaStoreError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
#[cfg_attr(not(target_family = "wasm"), async_trait)]
impl<T: MediaStore> MediaStore for EraseMediaStoreError<T> {
type Error = MediaStoreError;
async fn try_take_leased_lock(
&self,
lease_duration_ms: u32,
key: &str,
holder: &str,
) -> Result<Option<CrossProcessLockGeneration>, Self::Error> {
self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into)
}
async fn add_media_content(
&self,
request: &MediaRequestParameters,
content: Vec<u8>,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.add_media_content(request, content, ignore_policy).await.map_err(Into::into)
}
async fn replace_media_key(
&self,
from: &MediaRequestParameters,
to: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.replace_media_key(from, to).await.map_err(Into::into)
}
async fn get_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content(request).await.map_err(Into::into)
}
async fn remove_media_content(
&self,
request: &MediaRequestParameters,
) -> Result<(), Self::Error> {
self.0.remove_media_content(request).await.map_err(Into::into)
}
async fn get_media_content_for_uri(
&self,
uri: &MxcUri,
) -> Result<Option<Vec<u8>>, Self::Error> {
self.0.get_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<(), Self::Error> {
self.0.remove_media_content_for_uri(uri).await.map_err(Into::into)
}
async fn set_media_retention_policy(
&self,
policy: MediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.set_media_retention_policy(policy).await.map_err(Into::into)
}
fn media_retention_policy(&self) -> MediaRetentionPolicy {
self.0.media_retention_policy()
}
async fn set_ignore_media_retention_policy(
&self,
request: &MediaRequestParameters,
ignore_policy: IgnoreMediaRetentionPolicy,
) -> Result<(), Self::Error> {
self.0.set_ignore_media_retention_policy(request, ignore_policy).await.map_err(Into::into)
}
async fn clean(&self) -> Result<(), Self::Error> {
self.0.clean().await.map_err(Into::into)
}
async fn optimize(&self) -> Result<(), Self::Error> {
self.0.optimize().await.map_err(Into::into)
}
async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
self.0.get_size().await.map_err(Into::into)
}
}
/// A type-erased [`MediaStore`].
pub type DynMediaStore = dyn MediaStore<Error = MediaStoreError>;
/// A type that can be type-erased into `Arc<dyn MediaStore>`.
///
/// This trait is not meant to be implemented directly outside
/// `matrix-sdk-base`, but it is automatically implemented for everything that
/// implements `MediaStore`.
pub trait IntoMediaStore {
#[doc(hidden)]
fn into_media_store(self) -> Arc<DynMediaStore>;
}
impl IntoMediaStore for Arc<DynMediaStore> {
fn into_media_store(self) -> Arc<DynMediaStore> {
self
}
}
impl<T> IntoMediaStore for T
where
T: MediaStore + Sized + 'static,
{
fn into_media_store(self) -> Arc<DynMediaStore> {
Arc::new(EraseMediaStoreError(self))
}
}
// Turns a given `Arc<T>` into `Arc<DynMediaStore>` by attaching the
// `MediaStore` impl vtable of `EraseMediaStoreError<T>`.
impl<T> IntoMediaStore for Arc<T>
where
T: MediaStore + 'static,
{
fn into_media_store(self) -> Arc<DynMediaStore> {
let ptr: *const T = Arc::into_raw(self);
let ptr_erased = ptr as *const EraseMediaStoreError<T>;
// SAFETY: EraseMediaStoreError is repr(transparent) so T and
// EraseMediaStoreError<T> have the same layout and ABI
unsafe { Arc::from_raw(ptr_erased) }
}
}
+1 -1
View File
@@ -570,7 +570,7 @@ fn marks_as_unread(event: &Raw<AnySyncTimelineEvent>, user_id: &UserId) -> bool
match event {
AnySyncMessageLikeEvent::CallAnswer(_)
| AnySyncMessageLikeEvent::CallInvite(_)
| AnySyncMessageLikeEvent::CallNotify(_)
| AnySyncMessageLikeEvent::RtcNotification(_)
| AnySyncMessageLikeEvent::CallHangup(_)
| AnySyncMessageLikeEvent::CallCandidates(_)
| AnySyncMessageLikeEvent::CallNegotiate(_)
@@ -0,0 +1,72 @@
//! Data types used for handling the recently used emojis.
//!
//! There is no formal spec for this, only the implementation in Element Web:
//! <https://github.com/element-hq/element-web/commit/a7f92f35f5a27a53a5a030ea7c471be97751a67a>
use ruma::{UInt, events::macros::EventContent};
use serde::{Deserialize, Serialize};
/// An event type containing a list of recently used emojis for reactions.
#[cfg(feature = "experimental-element-recent-emojis")]
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "io.element.recent_emoji", kind = GlobalAccountData)]
pub struct RecentEmojisContent {
/// The list of recently used emojis, ordered by recency. The tuple of
/// `String`, `UInt` values represent the actual emoji and the number of
/// times it's been used in total, for those clients that might be
/// interested.
pub recent_emoji: Vec<(String, UInt)>,
}
#[cfg(feature = "experimental-element-recent-emojis")]
impl RecentEmojisContent {
/// Creates a new recent emojis event content given the provided recent
/// emojis.
pub fn new(recent_emoji: Vec<(String, UInt)>) -> Self {
Self { recent_emoji }
}
}
#[cfg(feature = "experimental-element-recent-emojis")]
#[cfg(test)]
mod tests {
use ruma::uint;
use serde_json::{from_value, json, to_value};
use crate::recent_emojis::RecentEmojisContent;
#[test]
fn serialization() {
let content = RecentEmojisContent::new(vec![
("😁".to_owned(), uint!(2)),
("🎉".to_owned(), uint!(10)),
]);
let json = to_value(&content).expect("recent emoji serialization failed");
let expected = json!({
"recent_emoji": [
["😁", 2],
["🎉", 10],
]
});
assert_eq!(json, expected);
}
#[test]
fn deserialization() {
let json = json!({
"recent_emoji": [
["😁", 2],
["🎉", 10],
]
});
let content =
from_value::<RecentEmojisContent>(json).expect("recent emoji deserialization failed");
let expected = RecentEmojisContent::new(vec![
("😁".to_owned(), uint!(2)),
("🎉".to_owned(), uint!(10)),
]);
assert_eq!(content.recent_emoji, expected.recent_emoji);
}
}
@@ -172,7 +172,7 @@ fn map_info<F: FnOnce(&mut RoomInfo)>(
let mut info = room.clone_info();
f(&mut info);
changes.add_room(info);
} else {
} else if store.already_logged_missing_room.lock().insert(room_id.to_owned()) {
debug!(room = %room_id, "couldn't find room in state changes or store");
}
}
@@ -14,7 +14,7 @@
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_crypto::RoomEventDecryptionResult;
use ruma::{RoomId, events::AnySyncTimelineEvent, serde::Raw};
use ruma::RoomId;
use super::{super::verification, E2EE};
use crate::Result;
@@ -26,21 +26,28 @@ use crate::Result;
/// application, returns `Err`.
///
/// Returns `Ok(None)` if encryption is not configured.
///
/// 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<'_>,
event: &Raw<AnySyncTimelineEvent>,
event: &TimelineEvent,
room_id: &RoomId,
) -> Result<Option<TimelineEvent>> {
let Some(olm) = e2ee.olm_machine else { return Ok(None) };
Ok(Some(
match olm
.try_decrypt_room_event(event.cast_ref_unchecked(), room_id, e2ee.decryption_settings)
.try_decrypt_room_event(
event.raw().cast_ref_unchecked(),
room_id,
e2ee.decryption_settings,
)
.await?
{
RoomEventDecryptionResult::Decrypted(decrypted) => {
// Note: the push actions are set by the caller.
let timeline_event = TimelineEvent::from_decrypted(decrypted, None);
let timeline_event = event.to_decrypted(decrypted, None);
if let Ok(sync_timeline_event) = timeline_event.raw().deserialize() {
verification::process_if_relevant(&sync_timeline_event, e2ee, room_id).await?;
@@ -48,9 +55,7 @@ pub async fn sync_timeline_event(
timeline_event
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => {
TimelineEvent::from_utd(event.clone(), utd_info)
}
RoomEventDecryptionResult::UnableToDecrypt(utd_info) => event.to_utd(utd_info),
},
))
}
@@ -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,204 +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::YesCallNotify(_)
| 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.
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) => {
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,6 @@ use std::collections::BTreeMap;
#[cfg(feature = "e2e-encryption")]
use std::collections::BTreeSet;
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::deserialized_responses::TimelineEvent;
use matrix_sdk_common::timer;
use ruma::{
JsOption, OwnedRoomId, RoomId, UserId,
@@ -30,11 +28,10 @@ use ruma::{
assign,
events::{
AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnySyncStateEvent,
room::member::{MembershipState, RoomMemberEventContent},
room::member::{MembershipState, PossiblyRedactedRoomMemberEventContent},
},
serde::Raw,
};
use tokio::sync::broadcast::Sender;
#[cfg(feature = "e2e-encryption")]
use super::super::e2ee;
@@ -42,13 +39,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::RawSyncStateEventWithKeys,
};
/// Represent any kind of room updates.
@@ -70,12 +65,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,7 +74,7 @@ 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;
@@ -96,12 +87,11 @@ pub async fn update_any_room(
#[allow(unused_mut)] // Required for some feature flag combinations
let (mut room, mut room_info, maybe_room_update_kind) = membership(
context,
&state_events,
&mut raw_state_events,
&invite_state_events,
state_store,
user_id,
room_id,
room_info_notable_update_sender,
);
room_info.mark_state_partially_synced();
@@ -115,7 +105,7 @@ 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,
@@ -132,6 +122,7 @@ pub async fn update_any_room(
(&raw_events, &events),
&room,
&mut room_info,
user_id,
notification::Notification::new(
notification.push_rules,
notification.notifications,
@@ -154,18 +145,6 @@ pub async fn update_any_room(
)
.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 +197,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 +215,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],
state_events: &mut [RawSyncStateEventWithKeys],
invite_state_events: &Option<(Vec<Raw<AnyStrippedStateEvent>>, Vec<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,23 +230,23 @@ 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| {
let own_membership_event = state_events.1.iter().find_map(|event| {
if let AnyStrippedStateEvent::RoomMember(membership_event) = event
&& membership_event.state_key == user_id
{
return Some(membership_event.content.clone());
}
None
});
match membership_event {
match own_membership_event {
// 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(PossiblyRedactedRoomMemberEventContent {
membership: 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();
@@ -275,11 +260,7 @@ 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();
@@ -294,61 +275,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,
@@ -399,7 +350,7 @@ fn properties(
}
if let Some(recency_stamp) = &room_response.bump_stamp {
let recency_stamp: u64 = (*recency_stamp).into();
let recency_stamp = u64::from(*recency_stamp).into();
if room_info.recency_stamp.as_ref() != Some(&recency_stamp) {
room_info.update_recency_stamp(recency_stamp);
@@ -418,147 +369,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::YesCallNotify(_)
| 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!(
"Failed to deserialize event as AnySyncTimelineEvent. ID={}",
event.event_id().expect("Event has no ID!")
);
}
}
// 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},
};
@@ -44,17 +43,12 @@ pub async fn update_joined_room(
notification: notification::Notification<'_>,
#[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,11 +61,11 @@ 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,
@@ -155,17 +149,12 @@ pub async fn update_left_room(
notification: notification::Notification<'_>,
#[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;
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,11 +162,11 @@ 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 (),
@@ -212,17 +201,13 @@ 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<'_>,
) -> 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);
@@ -235,6 +220,7 @@ pub async fn update_invited_room(
(&raw_events, &events),
&room,
&mut room_info,
user_id,
notification,
)
.await?;
@@ -248,17 +234,13 @@ 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<'_>,
) -> 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);
@@ -271,6 +253,7 @@ pub async fn update_knocked_room(
(&raw_events, &events),
&room,
&mut room_info,
user_id,
notification,
)
.await?;
@@ -14,40 +14,41 @@
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, warn};
use super::Context;
use crate::store::BaseStateStore;
#[cfg(feature = "experimental-encrypted-state-events")]
use super::e2ee;
use crate::{store::BaseStateStore, utils::RawSyncStateEventWithKeys};
/// 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::RawSyncStateEventWithKeys,
};
impl State {
@@ -58,18 +59,23 @@ pub mod sync {
pub(crate) fn collect(
&self,
timeline: &[Raw<AnySyncTimelineEvent>],
) -> (Vec<Raw<AnySyncStateEvent>>, Vec<AnySyncStateEvent>) {
) -> Vec<RawSyncStateEventWithKeys> {
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(RawSyncStateEventWithKeys::try_from_raw_state_event)
.chain(
timeline
.iter()
.filter_map(RawSyncStateEventWithKeys::try_from_raw_timeline_event),
)
.collect(),
Self::After(events) => events
.iter()
.cloned()
.filter_map(RawSyncStateEventWithKeys::try_from_raw_state_event)
.collect(),
}
}
}
@@ -86,7 +92,7 @@ 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<RawSyncStateEventWithKeys>,
room_info: &mut RoomInfo,
ambiguity_cache: &mut AmbiguityCache,
new_users: &mut U,
@@ -96,49 +102,41 @@ pub mod sync {
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 +144,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 +164,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 +176,19 @@ pub mod sync {
async fn dispatch_room_member<U>(
context: &mut Context,
room_id: &RoomId,
event: &SyncStateEvent<RoomMemberEventContent>,
raw_event: &mut RawSyncStateEventWithKeys,
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,20 +218,60 @@ 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 [RawSyncStateEventWithKeys],
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 ruma::{events::AnyStrippedStateEvent, push::Action};
use ruma::{RoomId, UserId, events::AnyStrippedStateEvent, push::Action};
use tracing::instrument;
use super::{
super::{notification, timeline},
Context, Raw,
};
use crate::{Result, Room, RoomInfo};
use crate::{Result, Room, RoomInfo, RoomInfoNotableUpdateReasons};
/// Collect [`Raw<AnyStrippedStateEvent>`] to [`AnyStrippedStateEvent`].
pub fn collect(
@@ -296,18 +302,22 @@ pub mod stripped {
(raw_events, events): (&[Raw<AnyStrippedStateEvent>], &[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);
state_events
.entry(event.event_type())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_owned(), raw_event.clone());
}
own_membership(context, room.room_id(), user_id, events);
context
.state_changes
.stripped_state
@@ -332,6 +342,36 @@ pub mod stripped {
Ok(())
}
/// 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,
state_events: &[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 state_events.iter().rev().any(|event| {
// Find the event that updates the current user's membership.
if let AnyStrippedStateEvent::RoomMember(member) = &event
&& member.state_key.as_str() == user_id.as_str()
{
true
} else {
false
}
}) {
// 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);
}
}
}
fn collect<'a, I, T>(raw_events: I) -> (Vec<Raw<T>>, Vec<T>)
@@ -354,16 +394,22 @@ where
/// 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 RawSyncStateEventWithKeys,
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 +420,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 +437,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 +463,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 RawSyncStateEventWithKeys,
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 +501,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,6 +518,67 @@ 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 RawSyncStateEventWithKeys,
room_id: &RoomId,
e2ee: &e2ee::E2EE<'_>,
) -> Option<RawSyncStateEventWithKeys> {
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 RawSyncStateEventWithKeys::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;
@@ -16,7 +16,7 @@ use matrix_sdk_common::{deserialized_responses::TimelineEvent, timer};
#[cfg(feature = "e2e-encryption")]
use ruma::events::SyncMessageLikeEvent;
use ruma::{
UInt, UserId, assign,
MilliSecondsSinceUnixEpoch, UInt, UserId, assign,
events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
push::{Action, PushConditionRoomCtx},
};
@@ -31,6 +31,7 @@ use crate::{Result, Room, RoomInfo, sync::Timeline};
///
/// For each event:
/// - will try to decrypt it,
/// - will fix the `origin_server_ts` if considered invalid,
/// - will process verification,
/// - will process redaction,
/// - will process notification.
@@ -46,6 +47,7 @@ pub async fn build<'notification, 'e2ee>(
) -> Result<Timeline> {
let _timer = timer!(tracing::Level::TRACE, "build a timeline from sync");
let now = MilliSecondsSinceUnixEpoch::now();
let mut timeline = Timeline::new(timeline_inputs.limited, timeline_inputs.prev_batch);
let mut push_condition_room_ctx = get_push_room_context(context, room, room_info).await?;
let room_id = room.room_id();
@@ -53,7 +55,7 @@ pub async fn build<'notification, 'e2ee>(
for raw_event in timeline_inputs.raw_events {
// Start by assuming we have a plaintext event. We'll replace it with a
// decrypted or UTD event below if necessary.
let mut timeline_event = TimelineEvent::from_plaintext(raw_event);
let mut timeline_event = TimelineEvent::from_plaintext_with_max_timestamp(raw_event, now);
// Do some special stuff on the `timeline_event` before collecting it.
match timeline_event.raw().deserialize() {
@@ -94,7 +96,7 @@ pub async fn build<'notification, 'e2ee>(
if let Some(decrypted_timeline_event) =
Box::pin(e2ee::decrypt::sync_timeline_event(
e2ee.clone(),
timeline_event.raw(),
&timeline_event,
room_id,
))
.await?
@@ -25,6 +25,15 @@ use crate::Result;
/// Process the given event as a verification event if it is a candidate. The
/// event must be decrypted.
///
/// **Note**: If the supplied event is an `m.room.message` event with
/// `msgtype: m.key.verification.request`, then the device information for
/// the sending user must be up-to-date before calling this method
/// (otherwise, the request will be ignored). It is hard to guarantee this
/// is the case, but you can maximize your chances by explicitly making a
/// request for this user's device info by calling
/// [`OlmMachine::query_keys_for_users`], sending the request, and
/// processing the response with [`OlmMachine::mark_request_as_sent`].
pub async fn process_if_relevant(
event: &AnySyncTimelineEvent,
e2ee: E2EE<'_>,
+32 -31
View File
@@ -20,7 +20,7 @@ impl Room {
/// Is there a non expired membership with application `m.call` and scope
/// `m.room` in this room.
pub fn has_active_room_call(&self) -> bool {
self.inner.read().has_active_room_call()
self.info.read().has_active_room_call()
}
/// Returns a `Vec` of `OwnedUserId`'s that participate in the room call.
@@ -32,7 +32,7 @@ impl Room {
///
/// The vector is ordered by oldest membership user to newest.
pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
self.inner.read().active_room_call_participants()
self.info.read().active_room_call_participants()
}
}
@@ -41,25 +41,26 @@ mod tests {
use std::{ops::Sub, sync::Arc, time::Duration};
use assign::assign;
use matrix_sdk_test::{ALICE, BOB, CAROL};
use matrix_sdk_test::{ALICE, BOB, CAROL, event_factory::EventFactory};
use ruma::{
DeviceId, EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, UserId, device_id, event_id,
events::{
AnySyncStateEvent, StateUnsigned, SyncStateEvent,
AnySyncStateEvent,
call::member::{
ActiveFocus, ActiveLivekitFocus, Application, CallApplicationContent,
CallMemberEventContent, CallMemberStateKey, Focus, LegacyMembershipData,
LegacyMembershipDataInit, LivekitFocus, OriginalSyncCallMemberEvent,
LegacyMembershipDataInit, LivekitFocus,
},
},
room_id,
serde::Raw,
time::SystemTime,
user_id,
};
use similar_asserts::assert_eq;
use super::super::{Room, RoomState};
use crate::store::MemoryStore;
use crate::{store::MemoryStore, utils::RawSyncStateEventWithKeys};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
@@ -99,19 +100,17 @@ mod tests {
memberships: Vec<LegacyMembershipData>,
ev_id: &EventId,
user_id: &UserId,
) -> AnySyncStateEvent {
) -> Raw<AnySyncStateEvent> {
let content = CallMemberEventContent::new_legacy(memberships);
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
content,
event_id: ev_id.to_owned(),
sender: user_id.to_owned(),
EventFactory::new()
.sender(user_id)
.event(content)
.state_key(CallMemberStateKey::new(user_id.to_owned(), None, false).as_ref())
.event_id(ev_id)
// we can simply use now here since this will be dropped when using a MinimalStateEvent
// in the roomInfo
origin_server_ts: timestamp(0),
state_key: CallMemberStateKey::new(user_id.to_owned(), None, false),
unsigned: StateUnsigned::new(),
}))
.server_ts(timestamp(0))
.into()
}
struct InitData<'a> {
@@ -123,7 +122,7 @@ mod tests {
ev_id: &EventId,
user_id: &UserId,
init_data: Option<InitData<'_>>,
) -> AnySyncStateEvent {
) -> Raw<AnySyncStateEvent> {
let application = Application::Call(CallApplicationContent::new(
"my_call_id_1".to_owned(),
ruma::events::call::member::CallScope::Room,
@@ -156,16 +155,15 @@ mod tests {
),
};
AnySyncStateEvent::CallMember(SyncStateEvent::Original(OriginalSyncCallMemberEvent {
content,
event_id: ev_id.to_owned(),
sender: user_id.to_owned(),
EventFactory::new()
.sender(user_id)
.event(content)
.state_key(state_key.as_ref())
.event_id(ev_id)
// we can simply use now here since this will be dropped when using a MinimalStateEvent
// in the roomInfo
origin_server_ts: timestamp(0),
state_key,
unsigned: StateUnsigned::new(),
}))
.server_ts(timestamp(0))
.into()
}
fn foci_and_application() -> (Application, Vec<Focus>) {
@@ -181,11 +179,14 @@ mod tests {
)
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
room.inner.update_if(|info| {
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 RawSyncStateEventWithKeys::try_from_raw_state_event(ev)
.expect("generated state event should be valid"),
);
}
res
});
@@ -210,7 +211,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
}
@@ -241,7 +242,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
}
@@ -275,7 +276,7 @@ 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());
+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()
}
}
+188 -66
View File
@@ -17,7 +17,7 @@ use std::fmt;
use as_variant::as_variant;
use regex::Regex;
use ruma::{
OwnedMxcUri, OwnedUserId, UserId,
OwnedMxcUri, OwnedUserId, RoomAliasId, UserId,
events::{SyncStateEvent, member_hints::MemberHintsEventContent},
};
use serde::{Deserialize, Serialize};
@@ -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)
@@ -57,7 +57,36 @@ impl Room {
///
/// This cache is refilled every time we call [`Self::display_name`].
pub fn cached_display_name(&self) -> Option<RoomDisplayName> {
self.inner.read().cached_display_name.clone()
self.info.read().cached_display_name.clone()
}
/// Computes the display name for a room using the provided fields.
///
/// This function is useful for reusing the same display name computation
/// logic where full Rooms aren't available e.g. space summary rooms.
pub fn compute_display_name_with_fields(
name: Option<String>,
canonical_alias: Option<&RoomAliasId>,
heroes: Vec<RoomHero>,
num_joined_members: u64,
) -> RoomDisplayName {
// Handle empty string names. The `Room` level implementation relies
// on `RoomInfo` doing the same thing.
let name = name.and_then(|name| (!name.is_empty()).then_some(name));
match (name, canonical_alias) {
(Some(name), _) => RoomDisplayName::Named(name.trim().to_owned()),
(None, Some(alias)) => RoomDisplayName::Aliased(alias.alias().trim().to_owned()),
(None, None) => {
let hero_display_names =
heroes.into_iter().filter_map(|hero| hero.display_name).collect::<Vec<_>>();
compute_display_name_from_heroes(
num_joined_members,
hero_display_names.iter().map(|name| name.as_str()).collect(),
)
}
}
}
/// Force recalculating a room's display name, taking into account its name,
@@ -70,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),
@@ -78,7 +107,7 @@ impl Room {
}
let display_name_or_summary = {
let inner = self.inner.read();
let inner = self.info.read();
match (inner.name(), inner.canonical_alias()) {
(Some(name), _) => {
@@ -107,7 +136,7 @@ impl Room {
// Update the cached display name before we return the newly computed value.
let mut updated = false;
self.inner.update_if(|info| {
self.info.update_if(|info| {
if info.cached_display_name.as_ref() != Some(&display_name) {
info.cached_display_name = Some(display_name.clone());
updated = true;
@@ -378,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
@@ -515,9 +544,11 @@ 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,
@@ -528,8 +559,7 @@ mod tests {
use super::{Room, RoomDisplayName, compute_display_name_from_heroes};
use crate::{
MinimalStateEvent, OriginalMinimalStateEvent, RoomState, StateChanges, StateStore,
store::MemoryStore,
MinimalStateEvent, RoomHero, RoomState, StateChanges, StateStore, store::MemoryStore,
};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
@@ -555,19 +585,23 @@ mod tests {
}
fn make_canonical_alias_event() -> MinimalStateEvent<RoomCanonicalAliasEventContent> {
MinimalStateEvent::Original(OriginalMinimalStateEvent {
content: assign!(RoomCanonicalAliasEventContent::new(), {
MinimalStateEvent {
content: assign!(PossiblyRedactedRoomCanonicalAliasEventContent::new(), {
alias: Some(room_alias_id!("#test:example.com").to_owned()),
}),
event_id: None,
})
}
}
fn make_name_event() -> MinimalStateEvent<RoomNameEventContent> {
MinimalStateEvent::Original(OriginalMinimalStateEvent {
content: RoomNameEventContent::new("Test Room".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<PossiblyRedactedRoomNameEventContent> {
make_name_event_with("Test Room")
}
#[async_test]
@@ -576,10 +610,34 @@ mod tests {
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
}
#[test]
fn test_display_name_compute_fields_empty() {
assert_eq!(
Room::compute_display_name_with_fields(None, None, vec![], 0),
RoomDisplayName::Empty
);
}
#[async_test]
async fn test_display_name_for_joined_room_is_empty_if_name_empty() {
let (_, room) = make_room_test_helper(RoomState::Joined);
room.info.update(|info| info.base_info.name = Some(make_name_event_with("")));
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
}
#[test]
fn test_display_name_compute_fields_empty_if_name_empty() {
assert_eq!(
Room::compute_display_name_with_fields(Some("".to_owned()), None, vec![], 0),
RoomDisplayName::Empty
);
}
#[async_test]
async fn test_display_name_for_joined_room_uses_canonical_alias_if_available() {
let (_, room) = make_room_test_helper(RoomState::Joined);
room.inner
room.info
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
@@ -587,16 +645,29 @@ mod tests {
);
}
#[test]
fn test_display_name_compute_fields_alias() {
assert_eq!(
Room::compute_display_name_with_fields(
None,
Some(room_alias_id!("#test:example.com")),
vec![],
0,
),
RoomDisplayName::Aliased("test".to_owned())
);
}
#[async_test]
async fn test_display_name_for_joined_room_prefers_name_over_alias() {
let (_, room) = make_room_test_helper(RoomState::Joined);
room.inner
room.info
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
RoomDisplayName::Aliased("test".to_owned())
);
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
room.info.update(|info| info.base_info.name = Some(make_name_event()));
// Display name wasn't cached when we asked for it above, and name overrides
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
@@ -604,6 +675,19 @@ mod tests {
);
}
#[test]
fn test_display_name_compute_fields_name_over_alias() {
assert_eq!(
Room::compute_display_name_with_fields(
Some("Test Room".to_owned()),
Some(room_alias_id!("#test:example.com")),
vec![],
0
),
RoomDisplayName::Named("Test Room".to_owned())
);
}
#[async_test]
async fn test_display_name_for_invited_room_is_empty_if_no_info() {
let (_, room) = make_room_test_helper(RoomState::Invited);
@@ -614,11 +698,8 @@ 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,
});
room.inner.update(|info| info.base_info.name = Some(room_name));
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);
}
@@ -626,7 +707,7 @@ mod tests {
#[async_test]
async fn test_display_name_for_invited_room_uses_canonical_alias_if_available() {
let (_, room) = make_room_test_helper(RoomState::Invited);
room.inner
room.info
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
@@ -637,13 +718,13 @@ mod tests {
#[async_test]
async fn test_display_name_for_invited_room_prefers_name_over_alias() {
let (_, room) = make_room_test_helper(RoomState::Invited);
room.inner
room.info
.update(|info| info.base_info.canonical_alias = Some(make_canonical_alias_event()));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
RoomDisplayName::Aliased("test".to_owned())
);
room.inner.update(|info| info.base_info.name = Some(make_name_event()));
room.info.update(|info| info.base_info.name = Some(make_name_event()));
// Display name wasn't cached when we asked for it above, and name overrides
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
@@ -670,7 +751,7 @@ mod tests {
changes.add_stripped_member(room_id, me, make_stripped_member_event(me, "Me"));
store.save_changes(&changes).await.unwrap();
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
RoomDisplayName::Calculated("Matthew".to_owned())
@@ -720,12 +801,12 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
store.save_changes(&changes).await.unwrap();
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
RoomDisplayName::Calculated("Matthew".to_owned())
@@ -755,12 +836,12 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
let member_hints_content =
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
changes
.state
.entry(room_id.to_owned())
@@ -771,7 +852,7 @@ mod tests {
store.save_changes(&changes).await.unwrap();
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
// Bot should not contribute to the display name.
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
@@ -801,11 +882,11 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into());
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
let member_hints_content =
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
changes
.state
.entry(room_id.to_owned())
@@ -816,7 +897,7 @@ mod tests {
store.save_changes(&changes).await.unwrap();
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
// Bot should not contribute to the display name.
assert_eq!(room.compute_display_name().await.unwrap().into_inner(), RoomDisplayName::Empty);
}
@@ -837,8 +918,8 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
store.save_changes(&changes).await.unwrap();
@@ -867,12 +948,12 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(bot.into(), f.member(bot).display_name("Bot").into_raw());
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
members.insert(bot.into(), f.member(bot).display_name("Bot").into());
let member_hints_content =
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into_raw();
f.member_hints(BTreeSet::from([bot.to_owned()])).sender(me).into();
changes
.state
.entry(room_id.to_owned())
@@ -914,10 +995,10 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(carol.into(), f.member(carol).display_name("Carol").into());
members.insert(bob.into(), f.member(bob).display_name("Bob").into());
members.insert(fred.into(), f.member(fred).display_name("Fred").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
store.save_changes(&changes).await.unwrap();
}
@@ -928,9 +1009,9 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
members.insert(alice.into(), f.member(alice).display_name("Alice").into());
members.insert(erica.into(), f.member(erica).display_name("Erica").into());
members.insert(denis.into(), f.member(denis).display_name("Denis").into());
store.save_changes(&changes).await.unwrap();
}
@@ -938,7 +1019,7 @@ mod tests {
joined_member_count: Some(7u32.into()),
heroes: vec![denis.to_owned(), carol.to_owned(), bob.to_owned(), erica.to_owned()],
});
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
@@ -946,6 +1027,47 @@ mod tests {
);
}
#[test]
fn test_display_name_compute_fields_name_deterministic() {
assert_eq!(
Room::compute_display_name_with_fields(
None,
None,
vec![
RoomHero {
user_id: user_id!("@alice:example.org").to_owned(),
display_name: Some("Alice".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@bob:example.org").to_owned(),
display_name: Some("Bob".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@carol:example.org").to_owned(),
display_name: Some("Carol".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@denis:example.org").to_owned(),
display_name: Some("Denis".to_owned()),
avatar_url: None,
},
RoomHero {
user_id: user_id!("@erica:example.org").to_owned(),
display_name: Some("Erica".to_owned()),
avatar_url: None,
},
],
1234,
),
RoomDisplayName::Calculated(
"Alice, Bob, Carol, Denis, Erica, and 1229 others".to_owned()
)
);
}
#[async_test]
async fn test_display_name_deterministic_no_heroes() {
let (store, room) = make_room_test_helper(RoomState::Joined);
@@ -971,10 +1093,10 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(carol.into(), f.member(carol).display_name("Carol").into_raw());
members.insert(bob.into(), f.member(bob).display_name("Bob").into_raw());
members.insert(fred.into(), f.member(fred).display_name("Fred").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(carol.into(), f.member(carol).display_name("Carol").into());
members.insert(bob.into(), f.member(bob).display_name("Bob").into());
members.insert(fred.into(), f.member(fred).display_name("Fred").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
store.save_changes(&changes).await.unwrap();
}
@@ -986,9 +1108,9 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(alice.into(), f.member(alice).display_name("Alice").into_raw());
members.insert(erica.into(), f.member(erica).display_name("Erica").into_raw());
members.insert(denis.into(), f.member(denis).display_name("Denis").into_raw());
members.insert(alice.into(), f.member(alice).display_name("Alice").into());
members.insert(erica.into(), f.member(erica).display_name("Erica").into());
members.insert(denis.into(), f.member(denis).display_name("Denis").into());
store.save_changes(&changes).await.unwrap();
}
@@ -1018,12 +1140,12 @@ mod tests {
.or_default()
.entry(StateEventType::RoomMember)
.or_default();
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into_raw());
members.insert(me.into(), f.member(me).display_name("Me").into_raw());
members.insert(matthew.into(), f.member(matthew).display_name("Matthew").into());
members.insert(me.into(), f.member(me).display_name("Me").into());
store.save_changes(&changes).await.unwrap();
room.inner.update_if(|info| info.update_from_ruma_summary(&summary));
room.info.update_if(|info| info.update_from_ruma_summary(&summary));
assert_eq!(
room.compute_display_name().await.unwrap().into_inner(),
RoomDisplayName::EmptyWas("Matthew".to_owned())
+24 -27
View File
@@ -19,13 +19,13 @@ use super::Room;
impl Room {
/// Get the encryption state of this room.
pub fn encryption_state(&self) -> EncryptionState {
self.inner.read().encryption_state()
self.info.read().encryption_state()
}
/// Get the `m.room.encryption` content that enabled end to end encryption
/// in the room.
pub fn encryption_settings(&self) -> Option<RoomEncryptionEventContent> {
self.inner.read().base_info.encryption.clone()
self.info.read().base_info.encryption.clone()
}
}
@@ -80,26 +80,23 @@ impl EncryptionState {
mod tests {
use std::{
ops::{Not, Sub},
str::FromStr,
sync::Arc,
time::Duration,
};
use assert_matches::assert_matches;
use matrix_sdk_test::ALICE;
use matrix_sdk_test::{ALICE, event_factory::EventFactory};
use ruma::{
EventEncryptionAlgorithm, MilliSecondsSinceUnixEpoch, OwnedEventId,
events::{
AnySyncStateEvent, EmptyStateKey, StateUnsigned, SyncStateEvent,
room::encryption::{OriginalSyncRoomEncryptionEvent, RoomEncryptionEventContent},
},
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::RawSyncStateEventWithKeys};
fn make_room_test_helper(room_type: RoomState) -> (Arc<MemoryStore>, Room) {
let store = Arc::new(MemoryStore::new());
@@ -117,11 +114,14 @@ mod tests {
.expect("date out of range")
}
fn receive_state_events(room: &Room, events: Vec<&AnySyncStateEvent>) {
room.inner.update_if(|info| {
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 RawSyncStateEventWithKeys::try_from_raw_state_event(ev)
.expect("generated state event should be valid"),
);
}
res
});
@@ -135,19 +135,16 @@ mod tests {
let encryption_content =
RoomEncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
let encryption_event = AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(
OriginalSyncRoomEncryptionEvent {
content: encryption_content,
event_id: OwnedEventId::from_str("$1234_1").unwrap(),
sender: ALICE.to_owned(),
// we can simply use now here since this will be dropped when using a
// MinimalStateEvent in the roomInfo
origin_server_ts: timestamp(0),
state_key: EmptyStateKey,
unsigned: StateUnsigned::new(),
},
));
receive_state_events(&room, vec![&encryption_event]);
let encryption_event = EventFactory::new()
.sender(*ALICE)
.event(encryption_content)
.state_key("")
.event_id(event_id!("$1234_1"))
// we can simply use now here since this will be dropped when using a MinimalStateEvent
// in the roomInfo
.server_ts(timestamp(0))
.into();
receive_state_events(&room, vec![encryption_event]);
assert_matches!(room.encryption_state(), EncryptionState::Encrypted);
}
@@ -157,7 +154,7 @@ mod tests {
let (_store, room) = make_room_test_helper(RoomState::Joined);
assert_matches!(room.encryption_state(), EncryptionState::Unknown);
room.inner.update_if(|info| {
room.info.update_if(|info| {
info.mark_encryption_state_synced();
false
+13 -269
View File
@@ -12,283 +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};
#[cfg(feature = "e2e-encryption")]
use ruma::{OwnedRoomId, events::AnySyncTimelineEvent, serde::Raw};
use ruma::MilliSecondsSinceUnixEpoch;
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.inner.read().latest_event.as_deref().cloned()
}
/// Return the [`LatestEventValue`] of this room.
pub fn new_latest_event(&self) -> LatestEventValue {
self.inner.read().new_latest_event.clone()
///
/// Note that it clones the [`LatestEventValue`]! This can add pressure
/// on the memory if used in a hot path.
pub fn latest_event(&self) -> LatestEventValue {
self.info.read().latest_event_value.clone()
}
/// 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()
/// Return the value of [`LatestEventValue::timestamp`].
pub fn latest_event_timestamp(&self) -> Option<MilliSecondsSinceUnixEpoch> {
self.info.read().latest_event_value.timestamp()
}
/// 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.inner.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()
}
}
+111 -37
View File
@@ -19,8 +19,9 @@ use std::{
};
use bitflags::bitflags;
use futures_util::future;
use ruma::{
MxcUri, OwnedUserId, UserId,
Int, MxcUri, OwnedUserId, UserId,
events::{
MessageLikeEventType, StateEventType,
ignored_user_list::IgnoredUserListEventContent,
@@ -35,7 +36,7 @@ use tracing::debug;
use super::Room;
use crate::{
MinimalRoomMemberEvent,
MinimalRoomMemberEvent, StoreError,
deserialized_responses::{DisplayName, MemberEvent},
store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
};
@@ -48,7 +49,7 @@ impl Room {
///
/// Returns true if no members are missing, false otherwise.
pub fn are_members_synced(&self) -> bool {
self.inner.read().members_synced
self.info.read().members_synced
}
/// Mark this Room as holding all member information.
@@ -57,14 +58,14 @@ impl Room {
/// about its members.
#[cfg(feature = "testing")]
pub fn mark_members_synced(&self) {
self.inner.update(|info| {
self.info.update(|info| {
info.members_synced = true;
});
}
/// Mark this Room as still missing member information.
pub fn mark_members_missing(&self) {
self.inner.update_if(|info| {
self.info.update_if(|info| {
// notify observable subscribers only if the previous value was false
mem::replace(&mut info.members_synced, false)
})
@@ -119,17 +120,17 @@ impl Room {
/// Returns the number of members who have joined or been invited to the
/// room.
pub fn active_members_count(&self) -> u64 {
self.inner.read().active_members_count()
self.info.read().active_members_count()
}
/// Returns the number of members who have been invited to the room.
pub fn invited_members_count(&self) -> u64 {
self.inner.read().invited_members_count()
self.info.read().invited_members_count()
}
/// Returns the number of members who have joined the room.
pub fn joined_members_count(&self) -> u64 {
self.inner.read().joined_members_count()
self.info.read().joined_members_count()
}
/// Get the `RoomMember` with the given `user_id`.
@@ -140,17 +141,26 @@ impl Room {
///
/// Async because it can read from storage.
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await? else {
debug!(%user_id, "Member event not found in state store");
return Ok(None);
let event = async {
let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
else {
debug!(%user_id, "Member event not found in state store");
return Ok(None);
};
Ok(Some(raw_event.deserialize()?))
};
let presence = async {
let raw_event = self.store.get_presence_event(user_id).await?;
Ok::<Option<PresenceEvent>, StoreError>(raw_event.and_then(|e| e.deserialize().ok()))
};
let event = raw_event.deserialize()?;
let profile = async { self.store.get_profile(self.room_id(), user_id).await };
let presence =
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let (Some(event), presence, profile) = future::try_join3(event, presence, profile).await?
else {
return Ok(None);
};
let display_names = [event.display_name()];
let room_info = self.member_room_info(&display_names).await?;
@@ -166,18 +176,23 @@ impl Room {
display_names: &'a [DisplayName],
) -> StoreResult<MemberRoomInfo<'a>> {
let max_power_level = self.max_power_level();
let power_levels = self.power_levels_or_default().await;
let power_levels = async { Ok(self.power_levels_or_default().await) };
let users_display_names =
self.store.get_users_with_display_names(self.room_id(), display_names).await?;
self.store.get_users_with_display_names(self.room_id(), display_names);
let ignored_users = self
.store
.get_account_data_event_static::<IgnoredUserListEventContent>()
.await?
.map(|c| c.deserialize())
.transpose()?
.map(|e| e.content.ignored_users.into_keys().collect());
let ignored_users = async {
Ok(self
.store
.get_account_data_event_static::<IgnoredUserListEventContent>()
.await?
.map(|c| c.deserialize())
.transpose()?
.map(|e| e.content.ignored_users.into_keys().collect()))
};
let (power_levels, users_display_names, ignored_users) =
future::try_join3(power_levels, users_display_names, ignored_users).await?;
Ok(MemberRoomInfo {
power_levels: power_levels.into(),
@@ -244,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()
}
}
@@ -261,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()
}
}
@@ -277,15 +292,13 @@ impl RoomMember {
return UserPowerLevel::Infinite;
};
let mut power_level = i64::from(power_level);
let normalized_power_level = if self.max_power_level > 0 {
normalize_power_level(power_level, self.max_power_level)
} else {
power_level
};
if self.max_power_level > 0 {
power_level = (power_level * 100) / self.max_power_level;
}
UserPowerLevel::Int(
power_level.try_into().expect("normalized power level should fit in Int"),
)
UserPowerLevel::Int(normalized_power_level)
}
/// Get the power level of this member.
@@ -468,3 +481,64 @@ impl RoomMemberships {
memberships
}
}
/// Scale the given `power_level` to a range between 0-100.
pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
let mut power_level = i64::from(power_level);
power_level = (power_level * 100) / max_power_level;
Int::try_from(power_level.clamp(0, 100))
.expect("We clamped the normalized power level so they must fit into the Int")
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
prop_compose! {
fn arb_int()(id in any::<i64>()) -> Int {
id.try_into().unwrap_or_default()
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10_000))]
#[test]
fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
let normalized = normalize_power_level(power_level, 1);
let normalized = i64::from(normalized);
assert!(normalized >= 0);
assert!(normalized <= 100);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(10_000))]
#[test]
fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
let normalized = normalize_power_level(power_level, max_level);
let normalized = i64::from(normalized);
assert!(normalized >= 0);
assert!(normalized <= 100);
}
}
#[test]
fn test_power_level_normalization_limits() {
let level = Int::MIN;
let normalized = normalize_power_level(level, 1);
let normalized = i64::from(normalized);
assert!(normalized >= 0);
assert!(normalized <= 100);
let level = Int::MAX;
let normalized = normalize_power_level(level, 1);
let normalized = i64::from(normalized);
assert!(normalized >= 0);
assert!(normalized <= 100);
}
}
+62 -68
View File
@@ -26,8 +26,6 @@ mod state;
mod tags;
mod tombstone;
#[cfg(feature = "e2e-encryption")]
use std::sync::RwLock as SyncRwLock;
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
@@ -39,13 +37,11 @@ pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
pub use encryption::EncryptionState;
use eyeball::{AsyncLock, SharedObservable};
use futures_util::{Stream, StreamExt};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_common::ring_buffer::RingBuffer;
pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
pub(crate) use room_info::SyncInfo;
pub use room_info::{
BaseRoomInfo, InviteAcceptanceDetails, RoomInfo, RoomInfoNotableUpdate,
RoomInfoNotableUpdateReasons, apply_redaction,
RoomInfoNotableUpdateReasons, RoomRecencyStamp, apply_redaction,
};
use ruma::{
EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
@@ -63,8 +59,6 @@ use ruma::{
},
room::RoomType,
};
#[cfg(feature = "e2e-encryption")]
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
use serde::{Deserialize, Serialize};
pub use state::{RoomState, RoomStateFilter};
pub(crate) use tags::RoomNotableTags;
@@ -73,7 +67,7 @@ pub use tombstone::{PredecessorRoom, SuccessorRoom};
use tracing::{info, instrument, warn};
use crate::{
Error, MinimalStateEvent,
Error,
deserialized_responses::MemberEvent,
notification_settings::RoomNotificationMode,
read_receipts::RoomReadReceipts,
@@ -91,21 +85,15 @@ pub struct Room {
/// Our own user ID.
pub(super) own_user_id: OwnedUserId,
pub(super) inner: SharedObservable<RoomInfo>,
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
pub(super) store: Arc<DynStateStore>,
pub(super) info: SharedObservable<RoomInfo>,
/// 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.)
/// A clone of the [`BaseStateStore::room_info_notable_update_sender`].
///
/// Currently, these are held in Room rather than RoomInfo, because we were
/// not sure whether holding too many of them might make the cache too
/// slow to load on startup. Keeping them here means they are not cached
/// to disk but held in memory.
#[cfg(feature = "e2e-encryption")]
pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
/// [`BaseStateStore::room_info_notable_update_sender`]: crate::store::BaseStateStore::room_info_notable_update_sender
pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
/// A clone of the state store.
pub(super) store: Arc<DynStateStore>,
/// A map for ids of room membership events in the knocking state linked to
/// the user id of the user affected by the member event, that the current
@@ -140,11 +128,7 @@ impl Room {
own_user_id: own_user_id.into(),
room_id: room_info.room_id.clone(),
store,
inner: SharedObservable::new(room_info),
#[cfg(feature = "e2e-encryption")]
latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
Self::MAX_ENCRYPTED_EVENTS,
))),
info: SharedObservable::new(room_info),
room_info_notable_update_sender,
seen_knock_request_ids_map: SharedObservable::new_async(None),
room_member_updates_sender,
@@ -158,7 +142,7 @@ impl Room {
/// Get a copy of the room creators.
pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
self.inner.read().creators()
self.info.read().creators()
}
/// Get our own user id.
@@ -168,18 +152,18 @@ impl Room {
/// Whether this room's [`RoomType`] is `m.space`.
pub fn is_space(&self) -> bool {
self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
}
/// Returns the room's type as defined in its creation event
/// (`m.room.create`).
pub fn room_type(&self) -> Option<RoomType> {
self.inner.read().room_type().map(ToOwned::to_owned)
self.info.read().room_type().map(ToOwned::to_owned)
}
/// Get the unread notification counts.
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
self.inner.read().notification_counts
self.info.read().notification_counts
}
/// Get the number of unread messages (computed client-side).
@@ -187,12 +171,12 @@ impl Room {
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_messages(&self) -> u64 {
self.inner.read().read_receipts.num_unread
self.info.read().read_receipts.num_unread
}
/// Get the detailed information about read receipts for the room.
pub fn read_receipts(&self) -> RoomReadReceipts {
self.inner.read().read_receipts.clone()
self.info.read().read_receipts.clone()
}
/// Get the number of unread notifications (computed client-side).
@@ -200,7 +184,7 @@ impl Room {
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_notifications(&self) -> u64 {
self.inner.read().read_receipts.num_notifications
self.info.read().read_receipts.num_notifications
}
/// Get the number of unread mentions (computed client-side), that is,
@@ -209,7 +193,7 @@ impl Room {
/// This might be more precise than [`Self::unread_notification_counts`] for
/// encrypted rooms.
pub fn num_unread_mentions(&self) -> u64 {
self.inner.read().read_receipts.num_mentions
self.info.read().read_receipts.num_mentions
}
/// Check if the room states have been synced
@@ -220,40 +204,40 @@ impl Room {
///
/// Returns true if the state is fully synced, false otherwise.
pub fn is_state_fully_synced(&self) -> bool {
self.inner.read().sync_info == SyncInfo::FullySynced
self.info.read().sync_info == SyncInfo::FullySynced
}
/// Check if the room state has been at least partially synced.
///
/// See [`Room::is_state_fully_synced`] for more info.
pub fn is_state_partially_or_fully_synced(&self) -> bool {
self.inner.read().sync_info != SyncInfo::NoState
self.info.read().sync_info != SyncInfo::NoState
}
/// Get the `prev_batch` token that was received from the last sync. May be
/// `None` if the last sync contained the full room history.
pub fn last_prev_batch(&self) -> Option<String> {
self.inner.read().last_prev_batch.clone()
self.info.read().last_prev_batch.clone()
}
/// Get the avatar url of this room.
pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
self.inner.read().avatar_url().map(ToOwned::to_owned)
self.info.read().avatar_url().map(ToOwned::to_owned)
}
/// Get information about the avatar of this room.
pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
self.inner.read().avatar_info().map(ToOwned::to_owned)
self.info.read().avatar_info().map(ToOwned::to_owned)
}
/// Get the canonical alias of this room.
pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
self.inner.read().canonical_alias().map(ToOwned::to_owned)
self.info.read().canonical_alias().map(ToOwned::to_owned)
}
/// Get the canonical alias of this room.
pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
self.inner.read().alt_aliases().to_owned()
self.info.read().alt_aliases().to_owned()
}
/// Get the `m.room.create` content of this room.
@@ -266,10 +250,7 @@ impl Room {
/// redacted, all fields except `creator` will be set to their default
/// value.
pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
match self.inner.read().base_info.create.as_ref()? {
MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
}
Some(self.info.read().base_info.create.as_ref()?.content.clone())
}
/// Is this room considered a direct message.
@@ -279,7 +260,7 @@ impl Room {
pub async fn is_direct(&self) -> StoreResult<bool> {
match self.state() {
RoomState::Joined | RoomState::Left | RoomState::Banned => {
Ok(!self.inner.read().base_info.dm_targets.is_empty())
Ok(!self.info.read().base_info.dm_targets.is_empty())
}
RoomState::Invited => {
@@ -316,41 +297,41 @@ impl Room {
/// us to re-find a DM with a user even if they have left, since we may
/// want to re-invite them.
pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
self.inner.read().base_info.dm_targets.clone()
self.info.read().base_info.dm_targets.clone()
}
/// If this room is a direct message, returns the number of members that
/// we're sharing the room with.
pub fn direct_targets_length(&self) -> usize {
self.inner.read().base_info.dm_targets.len()
self.info.read().base_info.dm_targets.len()
}
/// Get the guest access policy of this room.
pub fn guest_access(&self) -> GuestAccess {
self.inner.read().guest_access().clone()
self.info.read().guest_access().clone()
}
/// Get the history visibility policy of this room.
pub fn history_visibility(&self) -> Option<HistoryVisibility> {
self.inner.read().history_visibility().cloned()
self.info.read().history_visibility().cloned()
}
/// Get the history visibility policy of this room, or a sensible default if
/// the event is missing.
pub fn history_visibility_or_default(&self) -> HistoryVisibility {
self.inner.read().history_visibility_or_default().clone()
self.info.read().history_visibility_or_default().clone()
}
/// Is the room considered to be public.
///
/// May return `None` if the join rule event is not available.
pub fn is_public(&self) -> Option<bool> {
self.inner.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
}
/// Get the join rule policy of this room, if available.
pub fn join_rule(&self) -> Option<JoinRule> {
self.inner.read().join_rule().cloned()
self.info.read().join_rule().cloned()
}
/// Get the maximum power level that this room contains.
@@ -358,7 +339,7 @@ impl Room {
/// This is useful if one wishes to normalize the power levels, e.g. from
/// 0-100 where 100 would be the max power level.
pub fn max_power_level(&self) -> i64 {
self.inner.read().base_info.max_power_level
self.info.read().base_info.max_power_level
}
/// Get the current power levels of this room.
@@ -370,7 +351,7 @@ impl Room {
.ok_or(Error::InsufficientData)?
.deserialize()?;
let creators = self.creators().ok_or(Error::InsufficientData)?;
let rules = self.inner.read().room_version_rules_or_default();
let rules = self.info.read().room_version_rules_or_default();
Ok(power_levels_content.power_levels(&rules.authorization, creators))
}
@@ -383,7 +364,7 @@ impl Room {
}
// As a fallback, create the default power levels of a room.
let rules = self.inner.read().room_version_rules_or_default();
let rules = self.info.read().room_version_rules_or_default();
RoomPowerLevels::new(
RoomPowerLevelsSource::None,
&rules.authorization,
@@ -396,12 +377,12 @@ impl Room {
/// The returned string may be empty if the event has been redacted, or it's
/// missing from storage.
pub fn name(&self) -> Option<String> {
self.inner.read().name().map(ToOwned::to_owned)
self.info.read().name().map(ToOwned::to_owned)
}
/// Get the topic of the room.
pub fn topic(&self) -> Option<String> {
self.inner.read().topic().map(ToOwned::to_owned)
self.info.read().topic().map(ToOwned::to_owned)
}
/// Update the cached user defined notification mode.
@@ -410,7 +391,7 @@ impl Room {
/// cached result can be retrieved in
/// [`Self::cached_user_defined_notification_mode`].
pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
self.inner.update_if(|info| {
self.info.update_if(|info| {
if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
info.cached_user_defined_notification_mode = Some(mode);
@@ -426,7 +407,20 @@ impl Room {
/// This cache is refilled every time we call
/// [`Self::update_cached_user_defined_notification_mode`].
pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
self.inner.read().cached_user_defined_notification_mode
self.info.read().cached_user_defined_notification_mode
}
/// Removes any existing cached value for the user defined notification
/// mode.
pub fn clear_user_defined_notification_mode(&self) {
self.info.update_if(|info| {
if info.cached_user_defined_notification_mode.is_some() {
info.cached_user_defined_notification_mode = None;
true
} else {
false
}
})
}
/// Get the list of users ids that are considered to be joined members of
@@ -437,7 +431,7 @@ impl Room {
/// Get the heroes for this room.
pub fn heroes(&self) -> Vec<RoomHero> {
self.inner.read().heroes().to_vec()
self.info.read().heroes().to_vec()
}
/// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
@@ -468,19 +462,19 @@ impl Room {
/// Returns a boolean indicating if this room has been manually marked as
/// unread
pub fn is_marked_unread(&self) -> bool {
self.inner.read().base_info.is_marked_unread
self.info.read().base_info.is_marked_unread
}
/// Returns the [`RoomVersionId`] of the room, if known.
pub fn version(&self) -> Option<RoomVersionId> {
self.inner.read().room_version().cloned()
self.info.read().room_version().cloned()
}
/// Returns the recency stamp of the room.
///
/// Please read `RoomInfo::recency_stamp` to learn more.
pub fn recency_stamp(&self) -> Option<u64> {
self.inner.read().recency_stamp
pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
self.info.read().recency_stamp
}
/// Returns the details about an invite to this room if the invite has been
@@ -491,20 +485,20 @@ impl Room {
/// - `None` if we didn't join this room using an invite or the invite
/// wasn't accepted by this client.
pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
self.inner.read().invite_acceptance_details.clone()
self.info.read().invite_acceptance_details.clone()
}
/// Get a `Stream` of loaded pinned events for this room.
/// If no pinned events are found a single empty `Vec` will be returned.
pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
self.inner
self.info
.subscribe()
.map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
}
/// Returns the current pinned event ids for this room.
pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
self.inner.read().pinned_event_ids()
self.info.read().pinned_event_ids()
}
}

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