42 Commits

Author SHA1 Message Date
joel.poyet a907501c08 chore(release): prepare 26.08.0 release notes
Fold the member directory, profile editor, invite and status banner features into the unreleased 26.08.0 changelog section.
2026-08-06 13:41:23 +02:00
joel.poyet 7f43998fa5 Add profile editor, member invite and status banners (#4)
* feat(healthchat): add profile editor, member invite and status banners

Native 13-field profile editor with diff-only saves and avatar upload
through the HealthChat API (replaces the upstream display-name editor),
email invitation for approved accounts, and non-dismissible account
status banners in settings and the member directory.

* fix(healthchat): satisfy detekt and konsist rules

Konsist presenter helper shape, no '=' in state parameter docs, present-tense invite callback, and complete renderer KDoc.
2026-08-06 13:28:24 +02:00
joel.poyet edb3520c56 Add member directory tab to home navigation (#3)
* feat(healthchat): add member directory tab to home navigation

Third bottom-nav tab backed by the HealthChat API: searchable member
cards with native start-chat, approval-pending banner, retry on
failure and navigation into the native profile screens.

* fix(healthchat): stop tab back handler swallowing back presses

While a screen is pushed on top of Home, the still-composed tab
BackHandler intercepted back presses during the transition and
invisibly switched to the Chats tab. Gate it on the node's target
visibility in both the parent and Home's own backstack.

* fix(healthchat): satisfy detekt, ktlint and konsist rules

Import alias order, composable parameter order in HomeView, and konsist presenter helper shape.

* ci: pass the real repository name to the screenshot recorder

recordScreenshots.yml set GITHUB_REPOSITORY from `secrets.GITHUB_REPOSITORY`.
No such secret exists on this fork, so the expression expands to an empty
string and overrides the built-in environment variable of the same name.
recordScreenshots.sh checks it up front and exits 1 with "No repo specified",
so the workflow could never record anything here.

Use the `github.repository` context instead, which is always populated.

Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F

* Update screenshots

* Revert "Update screenshots"

This reverts commit b99e0d643f.

* Reapply "Update screenshots"

This reverts commit 85bceb6d67.

* test(screenshots): stop a LayoutLib teardown crash failing the shard tests

Paparazzi 2.0.0-alpha05 / LayoutLib 16.2.1 races on HandlerThread startup:
HandlerThread_Delegate.run calls Process.setThreadPriority, which throws
NoSuchMethodError for Thread.setPosixNicenessInternal and kills the
"android.bg" thread. If the test thread reaches getLooper() after that, it
gets null, BackgroundThread.ensureThreadReady() NPEs, and the static
initialiser of LatencyTracker fails. The JVM then marks that class erroneous
for the lifetime of the fork, so every later preview showing the soft
keyboard dies with NoClassDefFoundError.

LayoutLibErrorFilterStatement already swallowed the bare NoSuchMethodError,
but PaparazziLogger re-throws collected background errors from teardown()
wrapped in a MultipleFailuresException when there is more than one, and that
wrapper slipped through. A recent run reported 17 failures with zero image
comparisons failing and zero delta images written.

Unwrap the wrapper by reflection and swallow it only when every collected
cause matches the known-bug list; anything unrecognised, and anything that
cannot be decomposed, is rethrown.

Also close a hole this filter had from the start. Paparazzi runs the test as
`try { body } finally { teardown() }`, so a teardown error discards whatever
the body threw — including a genuine snapshot mismatch. renderFailureCapture
is registered inside the Paparazzi rule to record the body's exception so it
wins over any teardown error, and the filter refuses to swallow anything if
that rule is not registered. Two HorizontalFloatingToolbar previews have been
mismatching silently behind this, and may now surface as real failures.

Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F

---------

Co-authored-by: ElementBot <android@element.io>
2026-08-06 13:10:01 +02:00
joel.poyet 0c78553641 Merge pull request #16 from Novaloop-AG/ci/screenshot-fork-per-test-class
Give each screenshot test class its own JVM
2026-08-06 10:49:24 +02:00
joel.poyet 081904345a ci: give each screenshot test class its own JVM
There are five screenshot test classes (PreviewShard1..4Test plus
PreviewA11yTest) and each shard renders roughly 770 previews in a single
process. Gradle reuses a forked JVM for the next test class once
maxParallelForks is exhausted, so on the current six-core runners
(forks = 3) two classes run second, inside a process that has already
rendered a full shard.

That matches the observed failure exactly: PreviewShard2Test and
PreviewShard4Test fail 100% while shards 1 and 3 pass 100%, and the split
holds even for byte-identical preview states. The previous eight-core
runners computed forks = 4, reused only one JVM, and were green on this
same tree.

forkEvery = 1 restarts the JVM after every test class. It costs five JVM
startups and no extra peak memory, unlike raising maxParallelForks to 5,
which would need 10 GB of test heap on a 19 GiB runner with no swap.

Claude-Session: https://claude.ai/code/session_01MQbp1EPM4TSXq8P8Pyq44F
2026-08-06 10:41:22 +02:00
joel.poyet ce180ef25b Merge pull request #2 from Novaloop-AG/feature/healthchat-profile-fields
Show HealthChat profile fields on profile screens
2026-08-05 15:24:31 +02:00
joel.poyet c7800abb0f test(healthchat): satisfy konsist presenter helper rule
Konsist requires createPresenter helpers to omit the return type.
2026-08-05 14:13:04 +02:00
joel.poyet 1aeb4655bc feat(healthchat): show profile fields on profile screens
Renderer slot in UserProfileView (all three profile screens) shows
specialty, practice, address, contact links and further info from
the HealthChat API, with inline retry on failure.
2026-08-05 14:13:04 +02:00
joel.poyet 2db4c9847f Add HealthChat API service layer (#1)
* feat(healthchat): add HealthChat API service layer

New features/healthchat modules: typed Retrofit client with
per-request Matrix token auth, member directory, profile and
invite services, plus test fakes and fixtures.

* test(healthchat): use runCatchingExceptions

Detekt's RunCatchingNotAllowed rule forbids plain runCatching.
2026-08-05 14:13:00 +02:00
joel.poyet 621df1dedf Merge pull request #14 from Novaloop-AG/chore/rerecord-goldens-on-vm-runners
Re-record screenshot goldens on the new VM runners
2026-08-05 14:12:35 +02:00
ElementBot ac264ea08c Update screenshots 2026-08-04 22:30:06 +02:00
joel.poyet c0c0b48470 Merge pull request #9 from Novaloop-AG/feature/healthchat-remove-analytics-onboarding
Remove the analytics opt-in screen from onboarding
2026-08-04 18:42:11 +02:00
joel.poyet fa08091528 fix(lint): drop dead onboarding title overrides 2026-08-04 18:13:03 +02:00
joel.poyet 399adf2276 feat(analytics): disable analytics unless credentials are configured
Removes the onboarding opt-in screen, the Analytics settings entry and
the unused Posthog and Sentry SDKs.
2026-08-04 18:13:03 +02:00
joel.poyet 67fb6744fe Merge pull request #12 from Novaloop-AG/chore/rerecord-paparazzi-goldens
Re-record the stale Paparazzi screenshot goldens
2026-08-04 18:12:28 +02:00
joel.poyet 3e1010ba07 test(screenshots): re-record the 39 stale Paparazzi goldens
Several captured blank or half-laid-out screens; ShareLocation also lost its
map pin now that no MapTiler key is configured.
2026-08-04 17:40:13 +02:00
joel.poyet 1eb530e727 Merge pull request #13 from Novaloop-AG/ci/skip-codecov-upload
Skip the Codecov upload when no token is configured
2026-08-04 17:39:36 +02:00
joel.poyet 0cd363b1c3 ci: skip the codecov upload when no token is configured 2026-08-04 17:10:38 +02:00
joel.poyet 31611e5499 Merge pull request #11 from Novaloop-AG/fix/healthchat-launcher-icon-lint
Fix launcher icon lint errors in the HealthChat app icon
2026-08-04 16:08:20 +02:00
joel.poyet fdf142db60 fix(icon): ship the monochrome launcher icons as webp
Matches upstream's format, which is what tools/lint/lint.xml already
exempts from IconExpectedSize.
2026-08-04 14:57:23 +02:00
joel.poyet 96db91de86 Merge pull request #10 from Novaloop-AG/fix/ci-runners-and-permissions
Move releases to the persistent self-hosted runners and fix CI checkout
2026-08-04 14:45:50 +02:00
joel.poyet e74db93344 ci: cap Gradle workers so test JVMs fit the container 2026-08-04 13:36:52 +02:00
joel.poyet 8c4ef9264b ci: record why the Gradle heap is reduced 2026-08-04 12:55:48 +02:00
joel.poyet 409a1e5b90 ci: report runner capacity on the self-hosted jobs 2026-08-04 12:43:27 +02:00
joel.poyet 2013b86dfe ci: size the Gradle heap for the self-hosted containers
Upstream's -Xmx8g/4g is sized for hosted VMs; here the daemon is SIGKILLed
and Gradle reports "the daemon has disappeared".
2026-08-04 12:28:42 +02:00
joel.poyet cedd070bfe ci: rely on the runner image for the Android SDK
Drops the setup-android step from the release job and gates the swapfile
resize, which needs a hosted runner image and passwordless sudo.
2026-08-04 11:04:07 +02:00
joel.poyet 0fcd7bcfce ci: run the heavy Gradle jobs on the self-hosted runners
Requires the Android SDK in the runner image. Also gates the disk-cleanup
step to hosted runners, where it would otherwise strip the host.
2026-08-04 08:42:36 +02:00
joel.poyet 0799ad5b73 ci: disable the Label Community PRs job on this fork 2026-08-04 08:07:51 +02:00
joel.poyet 9475ec3a48 ci: grant contents:read so checkout works on this private fork 2026-08-04 07:11:37 +02:00
joel.poyet 2221bf2972 ci(release): move releases to the persistent self-hosted runners
novaloop-incus-large is the GARM ephemeral pool, whose runners were reaped
mid-job; [self-hosted, Linux, X64] matches four persistent org runners.
2026-08-04 07:11:37 +02:00
joel.poyet 05df27fc3a Merge pull request #8 from Novaloop-AG/fix/play-upload-review-flag
Fix Play Store upload rejected for changesNotSentForReview
2026-08-03 14:45:36 +02:00
joel.poyet b4629fff90 fix(release): drop changesNotSentForReview from Play upload
Play now sends this app's changes for review automatically and the Edits
API rejects the parameter, so the AAB uploaded but the edit never committed.
2026-08-03 13:44:34 +02:00
joel.poyet 7bbbde76d1 Merge pull request #7 from Novaloop-AG/chore/release-26.08.0
Set version 26.08.0
2026-08-03 13:26:25 +02:00
joel.poyet ec0303606a chore(release): set version 26.08.0 2026-08-03 11:27:07 +02:00
joel.poyet b7bfc6f751 Merge pull request #6 from Novaloop-AG/feature/healthchat-deeplinks
Point deeplinks and shared permalinks at HealthChat hosts
2026-08-03 11:10:28 +02:00
joel.poyet ada6fc457b feat(share): share user permalinks via to.healthchat.ch
Mention pills keep the matrix.to wire format other clients expect.
2026-08-03 10:53:51 +02:00
joel.poyet 57a2e585ba feat(deeplink): point deeplinks at HealthChat hosts
Claims app.healthchat.ch and to.healthchat.ch as App Links, registers the
healthchat:// scheme, and drops the unreachable mobile.element.io handling.
2026-08-03 10:53:35 +02:00
joel.poyet d6ea73140e docs(release): document self-hosted runner and heap sizing 2026-07-27 18:38:37 +02:00
joel.poyet a7e1217e82 fix(gplay): strip permissions Play Console rejects 2026-07-27 18:35:37 +02:00
joel.poyet 09b6d6341b ci(release): add HealthChat Play Store release pipeline 2026-07-27 18:35:36 +02:00
Sedat Qaja a1866b4b28 feat(branding): add HealthChat launcher icon and logo
Official cross-in-circle icon (same art as the iOS X app); onboarding
title localized for en/de/fr/it.
2026-07-16 14:13:00 +02:00
Sedat Qaja 2994430b88 feat(branding): rebrand app identity to HealthChat
App id ch.healthchat.element, locked matrix.healthchat.ch homeserver.
2026-07-16 11:03:52 +02:00
5779 changed files with 26240 additions and 78588 deletions
+8 -12
View File
@@ -261,12 +261,12 @@ compliance metadata review.
- **Production rollout gone wrong** → Play Console → Production → **Halt
rollout** (works during staged rollout), then ship a hotfix as above.
## Known branch divergence when syncing with upstream
## Known branch divergence when merging upstream
Upstream is taken by **rebasing** the HealthChat commits onto the new release
tag, never by merging — the procedure, Git LFS handling and force-push live in
the `syncing-upstream-healthchat` skill. The HealthChat-only facts that must
survive every sync:
When merging a newer upstream Element X release tag into `release/healthchat`
(temporarily add `https://github.com/element-hq/element-x-android` as a
remote, fetch, merge, remove the remote — never keep a permanent upstream
remote):
- **`.github/workflows/healthchat-release-playstore.yml`**,
**`HEALTHCHAT_CHANGES.md`**, and
@@ -279,13 +279,9 @@ survive every sync:
Upstream uses `io.element.android.x` / `Element X` / nulls.
- **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` /
`DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`.
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** — the
homeserver lock: `accountProviderAllowList() = listOf(Generic("matrix.healthchat.ch"))`,
`canConnectToAnyAccountProvider() = false`, `isAllowedToConnectToAccountProvider()`
= membership in that list (since upstream 26.10.0; before that
`defaultHomeserverList()`). Upstream returns an empty list / `true` / `true`.
`BuildTimeConfig.OAUTH_CLIENT_URL_PATH` must stay `null` (upstream:
`"apps/android"`), or MAS rejects the redirect scheme.
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** —
`defaultHomeserverList() = listOf("matrix.healthchat.ch")` must survive.
Upstream returns an empty list.
- **`app/build.gradle.kts`** — the `healthchat` signing config and the
`appicon/healthchat` dependency must survive; upstream signs release with
the debug keystore and depends on `appicon/element`.
@@ -1,201 +0,0 @@
---
name: syncing-upstream-healthchat
description: Bring the HealthChat fork (release/healthchat) onto a newer element-hq/element-x-android release tag. Use when the user asks to rebase, sync, merge or update to upstream, take a new Element X version, or asks why the fork is behind upstream. Covers the patch-stack model, the rebase procedure, Git LFS, conflicts, verification, the force-push and the rewind.
---
# Syncing the HealthChat fork with upstream
`release/healthchat` = upstream release tag + HealthChat commits on top. Patch stack, same as
healthchat-web and element-x-ios. A sync replaces the stack's base and force-pushes the branch.
That is the one sanctioned history rewrite in this repo; everything else in `AGENTS.md` still
holds. We rebase, we never merge upstream into the branch.
First done v26.07.0 → v26.10.0 (2026-10-16 to 10-19). Numbers below come from that run.
## Before touching anything
- [ ] **Toolchain.** Upstream pins the JDK (`java-version` in `.github/workflows/*.yml`, 21),
`COMPILE_SDK` / `BUILD_TOOLS_VERSION` (`plugins/src/main/kotlin/Versions.kt`), AGP, Kotlin
and Metro (`gradle/libs.versions.toml`). Gradle and AGP install themselves; the Rust SDK
comes from Maven. What does not: the self-hosted incus runners ship no Android SDK — the
`Set up Android SDK` step installs it (see the releasing skill, "Runner and build tuning").
If `COMPILE_SDK` moved, check that step still resolves the platform. 26.10.0 moved nothing.
- [ ] **Dev Mac Gradle.** A warm daemon can hang `assembleDebug` forever (0 % CPU, no log).
`./gradlew --stop` first, then compile-only tasks with `--max-workers=2`, e.g.
`:tests:uitests:compileDebugUnitTestKotlin` compiles every module. Gate scripts on the
exit code — a `rebase --continue` after an unchecked compile committed a broken stop once.
- [ ] **Upstream.** There is no upstream remote by default. Tags are `v<version>`.
`git fetch https://github.com/element-hq/element-x-android.git tag v<new> --no-tags` is
enough for the rebase. LFS needs a named remote:
`git remote add upstream https://github.com/element-hq/element-x-android.git`. After
adding it, `gh` may resolve to upstream — always pass `-R Novaloop-AG/element-x-android`.
- [ ] **Decisions, product owner:** which upstream feature-flag defaults flipped
(`git diff v<old> v<new> -- libraries/featureflag/api/.../FeatureFlags.kt`; 26.10.0:
message search default on, kept on), where fork UI lands when upstream restructures a
screen (26.10.0: settings split, HealthChat entries went to the account section), and
whether fork commits may be squashed (only within one HealthChat release, never across).
## The stack
```bash
git log --oneline --no-merges v<old>..release/healthchat # the fork's commits
git merge-base --is-ancestor v<old> release/healthchat # must be true
```
Upstream files the fork edits — conflicts live here every time:
- `features/enterprise/impl-foss/.../DefaultEnterpriseService.kt` + test — the homeserver
lock (since 26.10.0: `accountProviderAllowList()` = `Generic("matrix.healthchat.ch")`,
`canConnectToAnyAccountProvider() = false`, membership check).
- `plugins/src/main/kotlin/config/BuildTimeConfig.kt` — app id, name, OIDC URLs,
`OAUTH_CLIENT_URL_PATH = null` (MAS: redirect scheme must reverse-match the apex host).
- `plugins/src/main/kotlin/Versions.kt` — version; dropped from the stack, set again at release.
- `features/home/impl` (Members tab: `HomeView`, `HomePresenter`, `HomeState`,
`HomeStatePreviewParam`, `HomeNavigationBarItem`, `HomeTopBar`, `HomeFlowNode`, tests) and
`features/home/api/HomeEntryPoint.kt`, `appnav/.../LoggedInFlowNode.kt` (`navigateToUserProfile`).
- `features/preferences/impl` root node/view + `PreferencesFlowNode` (profile editor replaces
the display-name editor, account status banner).
- `features/userprofile`, `features/roomdetails` (profile fields section).
- `libraries/deeplink/impl`, `libraries/matrix/{api,impl}` permalink (HealthChat hosts),
`features/login/impl/DefaultLoginIntentResolver.kt`.
- `.github/workflows/{tests,quality,maestro-local,recordScreenshots}.yml` (self-hosted
runners, heap, fork guards), `app/src/gplay/AndroidManifest.xml` (permission strip),
`HEALTHCHAT_CHANGES.md`.
## Rebuilding the stack
Measure first: dry-run `git rebase --onto v<new> v<old> HEAD` in a throwaway worktree with a
script that records the conflicting files per commit and resolves blindly, then abort. The
conflict map decides the cleanup. 26.10.0: 11 of 31 commits stopped, 29 hunks, 62 goldens.
**1. Clean the series on the old base** (scratch branch off `release/healthchat`). Interactive
rebase is not available to agents; rebuild with `cherry-pick` (`-n` + `commit -C <orig>` for
edited commits; `cherry-pick` has no `-q`). Drop golden-only commits and the version bump;
strip every PNG, and edits to files upstream deleted, out of feature commits; squash the CI
run-of-commits within one release. `git diff --stat release/healthchat` must show only the
intended removals; compile once.
**2. Plain rebase** — `git rebase --onto v<new> v<old> <scratch>`, resolve per stop, keep each
commit's subject and intent. Upstream API removals cannot be "resolved", they need new code
*inside that commit* (26.10.0: `EnterpriseService` redesign). After a large stop compile its
modules before `--continue`. Cross-commit fallout (renames, moved fakes, signature changes)
goes into ONE trailing `Adapt HealthChat to upstream v<new>` commit, not amended into owners —
every amend replays the rest. A mistake in an already-committed stop: `git commit --fixup=<sha>`,
then `GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <sha>^` (conflict-free when later
commits do not touch the file). Intermediates may not compile; the tip must.
**3. Review** — `git range-diff v<old>..release/healthchat v<new>..<scratch>`. `!` on a commit
that applied cleanly is context drift; check the `-`/`+` lines are not ours.
Recurring fallout to expect: Konsist naming (`*PreviewParam` for `PreviewParameterProvider`
subclasses, `*Event` for sealed interfaces), design-system signature changes (26.10.0:
`ListItem(headlineContent)` → `ListItem(content)`), test fakes moving modules, action pins in
our CI hunks regressing upstream's bumps, exhaustive `when` over `HomeNavigationBarItem`.
zsh: `for c in $VAR` does not split on newlines, a variable named `path` *is* `PATH`, and
`$TAG:file` applies a history modifier — use `while read`, never name a variable `path`,
write `${TAG}:file`.
## Git LFS — the part that bites
Goldens, compound screenshots, `docs/images-lfs`, and `libraries/mediaupload/impl` test assets
are LFS-tracked (`.gitattributes`, ~4500 files). A fresh clone holds pointers only.
- origin is a standalone repo; upstream's LFS objects are not there. GitHub rejects a push
whose *new commits* reference LFS objects it lacks (`GH008 … unknown Git LFS objects`). The
rebased branch carries every upstream commit since the old tag. Fetch the LFS tree of each
upstream commit that touched LFS paths, then push the objects before the branch:
```bash
git log --format=%H v<old>..v<new> -- '*.png' '*.jpg' '*.gif' '*.mp4' '*.m4a' '*.webp' \
| sort -u | while read c; do git lfs fetch upstream "$c"; done
git lfs push origin <scratch> # must report no "(missing)"; refetch that commit
git push origin <scratch>
```
26.10.0: 210 such commits, 9837 objects, 525 MB locally, 3809 uploaded (174 MB). Run the loop
detached (`nohup`) — an agent's tool call times out at 10 minutes — and keep a done-file so it
resumes. Objects still "(missing)" after the loop are usually goldens from the fork's *own*
commits that a pointer-only clone never held: `git lfs fetch origin <original commit sha>`
for each commit on the old branch that touched them. The dry run lists every object until
the branch itself is pushed; verify presence with the LFS batch API or trust a clean upload.
- `git lfs push <branch>` only covers objects introduced by commits absent from the remote. 26.10.0:
167 objects referenced by the v26.10.0 tree were outside that set and `Validate Git LFS` failed
with "Object does not exist on the server". Read the oids from that job's log and
`git lfs push --object-id origin <oid…>` them (they are local after the fetch loop). Treat a
green `Validate Git LFS` on the PR as the only proof the store is complete.
- The recorder commits as ElementBot, a non-member: its CI runs land in `action_required` and
need `gh api -X POST repos/<repo>/actions/runs/<id>/approve` (or the PR's "Approve and run").
Never push to the branch while a recording runs: the recorder's final `git push` is a plain
push and is rejected once the tip has moved.
- `git lfs checkout` before running `libraries/mediaupload/impl` tests locally; pointer files
fail them. The screenshot tests are never run locally (`AGENTS.md`).
- `validate-lfs.yml` fails a PR that commits a PNG outside LFS.
## Screenshots
Take upstream's goldens on every conflict and never commit locally rendered ones (macOS
renders differ). After the push, add the `Record-Screenshots` label to the PR or
`workflow_dispatch` `recordScreenshots.yml`; the Linux runner records and commits all goldens.
Review only the HealthChat ones. A PR whose base is `release/healthchat` is *conflicting* by
construction (old and new stack touch the same files) and GitHub runs no `pull_request`
workflow on a conflicting PR — open the PR against a branch at the upstream tag
(`upstream/v<new>`, pushed by the owner) so the PR shows the 19-odd stack commits and CI runs.
Retargeting a PR is an `edited` event: tests/quality/build only start on the next push.
The label path needs `persist-credentials: true` on the LFS checkout steps (private fork,
fixed in 26.10.0). If the job dies on the LayoutLib static-initialiser crash,
restore the "one JVM per screenshot test class" workaround (`forkEvery = 1L` in the root
`build.gradle.kts`, dropped in 26.10.0 because upstream's Paparazzi bump fixed it).
## Verification of the tip
Local, all green before handover (26.10.0: 1200 unit tests):
```bash
./gradlew :tests:uitests:compileDebugUnitTestKotlin :app:compileGplayDebugKotlin
./gradlew :features:healthchat:impl:testDebugUnitTest :features:home:impl:testDebugUnitTest \
:features:preferences:impl:testDebugUnitTest :features:enterprise:impl-foss:testDebugUnitTest \
:features:login:impl:testDebugUnitTest :libraries:matrix:impl:testDebugUnitTest :appnav:testDebugUnitTest
./gradlew :tests:konsist:test ktlintCheck detekt lint
```
CI on the branch (tests, quality, screenshots) must be green on the self-hosted runners.
Device matrix (owner, debug and release build): MAS OIDC login on a fresh install — chooser
skipped, client URI `https://healthchat.ch` without appended path — is the gate for everything
else; previous release → new keeps the session; last Classic build → new lands on login without
crashing; member directory, profile editor, invites, status banners; deeplinks on HealthChat
hosts and `to.healthchat.ch` share links; push in background and killed; release build under
R8 exercising the suspend Retrofit calls; whatever upstream switched on (26.10.0: German
whole-word search in an encrypted room).
## Force-push, rewind
```bash
git push origin release/healthchat:refs/heads/backup/release-healthchat-before-<new>
git push --force-with-lease=release/healthchat:<old tip> origin <scratch>:release/healthchat
git checkout release/healthchat && git reset --hard origin/release/healthchat # every clone
```
`release/healthchat` is the default branch and the repository ruleset `healthchat-base`
(id 20385846, rules `deletion` + `non_fast_forward`, no bypass actors) rejects the force-push with
`GH013 … Cannot force-push to this branch`. Classic branch protection is empty, so
`gh api repos/<repo>/branches/<branch>/protection` says "not protected" — check
`gh api repos/<repo>/rules/branches/release%2Fhealthchat` instead. Decision (2026-10-20): disable the
ruleset for the push and re-enable it right after, no permanent bypass:
```bash
gh api -X PUT repos/Novaloop-AG/element-x-android/rulesets/20385846 -f enforcement=disabled
git push --force-with-lease=release/healthchat:<old tip> origin <stack>:release/healthchat
gh api -X PUT repos/Novaloop-AG/element-x-android/rulesets/20385846 -f enforcement=active
gh api repos/Novaloop-AG/element-x-android/rulesets/20385846 -q .enforcement # must print active
```
Old `healthchat-v*` tags keep their commits alive and off the branch line. Rewind =
`git push --force origin healthchat-v<last>^{commit}:release/healthchat`. A rewind does not
remove a tag or Play build made on the new history — tag only after the device tests. Then set
the version, update `HEALTHCHAT_CHANGES.md`, the releasing skill ("Known branch divergence")
and this skill with what changed.
## What not to do
- Merge upstream into `release/healthchat`, or squash the fork into a side branch and merge
upstream there: both duplicate every fork commit and conflict on every touched file.
- Pull on a rewritten branch (merges the old history back in). Fetch and reset.
- Commit goldens from a Mac, or resolve a golden conflict by hand — take upstream's and record.
- Amend compile fixes into owner commits mid-rebase; collect them in the adaptation commit.
- Touch `release/healthchat` before the owner has reviewed the range-diff.
+4 -4
View File
@@ -69,18 +69,18 @@ body:
description: |
Trigger the defect, then click on the menu from the room list then "Report a bug". Describe the issue, mentioning that you have also filed a bug (it's helpful if you can include a link to the bug). Send the report to submit anonymous logs to the developers.
options:
- 'No'
- 'Yes'
- 'No'
validations:
required: true
- type: dropdown
id: pr
attributes:
label: Will you submit a Pull Request?
label: Are you willing to provide a PR?
description: |
Submitting a Pull Request can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
Providing a PR can drastically speed up the process of fixing this bug. Don't worry, it's still OK to answer 'No' :).
options:
- 'No'
- 'Yes'
- 'No'
validations:
required: true
+2 -2
View File
@@ -1,11 +1,11 @@
name: Enhancement request
description: Do you have a suggestion or feature request? Please open issue on the repo element-hq/element-meta if the request is global to all Element clients.
description: Do you have a suggestion or feature request?
labels: [T-Enhancement]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please open an issue on the [element-meta repository](https://github.com/element-hq/element-meta/issues/new?template=enhancement.yml) instead.
Thank you for taking the time to propose an enhancement to an existing feature. If you would like to propose a new feature or a major cross-platform change, please [start a discussion here](https://github.com/element-hq/element-meta/discussions/new?category=ideas).
- type: textarea
id: usecase
attributes:
-4
View File
@@ -28,10 +28,6 @@ changelog:
labels:
- PR-Dependencies
- title: 💼 Element Pro
labels:
- PR-Element-Pro
- title: Others
labels:
- PR-misc
+1 -22
View File
@@ -7,17 +7,10 @@
"PR-Dependencies",
],
"ignoreDeps": [
"string:app_name",
"gradle",
],
"minimumReleaseAge": "7 days",
"hostRules": [
{
"matchHost": "https://repo1.maven.org/maven2/",
"concurrentRequestLimit": 5,
"maxRequestsPerSecond": 5,
"timeout": 10000,
}
],
"packageRules": [
{
"matchPackageNames": [
@@ -25,20 +18,6 @@
"/^io.element.android/",
],
"minimumReleaseAge": null,
// Max default value is 6, with this we give absolute priority to our own dependencies
"prPriority": 7,
"registryUrls": [ "https://repo1.maven.org/maven2/" ],
},
{
// Fetch Android dependencies from Google's Maven repo instead of Maven Central.
"matchPackageNames": [
"/^androidx\\./",
"/^android\\./",
"/^com\\.android\\.tools/",
"/^com\\.google\\.firebase/",
"/^com\\.google\\.android/",
],
"registryUrls": [ "https://dl.google.com/android/maven2/" ],
},
{
"groupName": "kotlin",
+5 -16
View File
@@ -61,19 +61,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs
@@ -91,7 +91,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-debug
path: |
@@ -121,18 +121,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Compile release sources
if: ${{ matrix.variant == 'release' }}
run: |
./gradlew assembleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Run aapt dump
if: ${{ matrix.variant == 'release' }}
run: |
$ANDROID_HOME/build-tools/37.0.0/aapt dump badging \
./app/build/outputs/apk/gplay/release/app-gplay-universal-release.apk \
> /tmp/currentAaptDump.txt
- name: Check manifest has not changed
if: ${{ matrix.variant == 'release' }}
run: diff /tmp/currentAaptDump.txt ./tools/manifest/gplay/release/aaptDump.txt
run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile nightly sources
if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
+4 -4
View File
@@ -57,7 +57,7 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -70,12 +70,12 @@ jobs:
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK
@@ -93,7 +93,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug Enterprise APKs
if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-enterprise-debug
path: |
+37
View File
@@ -0,0 +1,37 @@
name: Danger CI
on: [pull_request, merge_group]
permissions:
# Private fork: actions/checkout needs contents:read to clone. Upstream is public,
# where an empty token can still read the repo, so an empty permissions block works there and
# fails here with `fatal: repository not found`.
contents: read
jobs:
build:
runs-on: ubuntu-latest
name: Danger main check
# Skip in forks, it doesn't work even with the fallback token
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- run: |
npm install --save-dev @babel/plugin-transform-flow-strip-types
- name: Danger
uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8
with:
args: "--dangerfile ./tools/danger/dangerfile.js"
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
# Fallback for forks
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -3
View File
@@ -19,16 +19,16 @@ jobs:
- name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Run World screenshots generation script
+3 -2
View File
@@ -13,11 +13,12 @@ jobs:
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
@@ -65,7 +65,7 @@ jobs:
# "The operation was canceled" and no other diagnostic.
runs-on: [self-hosted, Linux, X64]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
@@ -85,13 +85,10 @@ jobs:
- name: Resolve marketing version
id: version
env:
# Passed through the environment, never expanded into the script (zizmor: template-injection).
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="${{ github.event.inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#healthchat-v}"
fi
@@ -181,7 +178,7 @@ jobs:
done
- name: Use JDK 21
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
@@ -195,10 +192,7 @@ jobs:
# (SdkConstants.CURRENT_BUILD_TOOLS_VERSION), since nothing sets buildToolsVersion
# here. Both need refreshing in the image when AGP or COMPILE_SDK moves.
- name: Configure gradle
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
with:
# Release builds must not consume a cache another workflow could have poisoned.
cache-disabled: true
uses: gradle/actions/setup-gradle@v4
- name: Decode keystore
run: |
@@ -228,7 +222,7 @@ jobs:
- name: Upload workflow artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@v4
with:
name: healthchat-${{ steps.version.outputs.version }}
path: |
@@ -238,7 +232,7 @@ jobs:
if-no-files-found: warn
- name: Upload to Play Store
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: ch.healthchat.element
+8 -27
View File
@@ -31,9 +31,6 @@ jobs:
build-apk:
name: Build APK
runs-on: [self-hosted, Linux, X64]
# Secrets are not available to pull_request runs from forks, so the Maestro job below
# cannot run for them; skip the build too, as its only consumer is that job.
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
concurrency:
group: ${{ format('maestro-build-{0}', github.ref) }}
cancel-in-progress: true
@@ -55,19 +52,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }}
persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
- uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
name: Use JDK 21
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK
@@ -77,7 +74,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- name: Upload APK as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-apk-maestro
path: |
@@ -90,16 +87,13 @@ jobs:
name: Maestro test suite
runs-on: ubuntu-latest
needs: [ build-apk ]
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
# Allow only one to run at a time, since they use the same environment.
# Otherwise, tests running in parallel can break each other.
# queue: max lets several runs wait their turn instead of the default, where a
# newly queued run cancels the one already pending (that run then never gets a verdict).
concurrency:
group: maestro-test
queue: max
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -118,7 +112,7 @@ jobs:
run: curl -fsSL "https://get.maestro.mobile.dev" | bash
- name: Run Maestro tests in emulator
id: maestro_test
uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
continue-on-error: true
env:
MAESTRO_USERNAME: maestroelement
@@ -138,21 +132,8 @@ jobs:
disk-size: 3G
script: |
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk
- name: Set up Python
if: always()
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.14
- name: End all the remaining sessions of the Maestro account
if: always()
continue-on-error: true
env:
MAS_PASSWORD: ${{ secrets.MATRIX_MAESTRO_ACCOUNT_PASSWORD }}
run: |
pip install requests
python3 ./tools/mas/delete_all_sessions.py --homeserver matrix.org --username maestroelement
- name: Upload test results
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-results
path: |
+2 -2
View File
@@ -32,11 +32,11 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
+9 -9
View File
@@ -37,28 +37,28 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: false
- name: ⚙️ Run unit tests, debug and release
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES --continue
run: ./gradlew test $CI_GRADLE_ARG_PROPERTIES
- name: 📸 Run screenshot tests
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES --continue
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES
- name: 📈 Generate kover report and verify coverage
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: ✅ Upload kover report
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: kover-results
path: |
@@ -76,23 +76,23 @@ jobs:
name: Dependency analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
- name: Upload dependency analysis
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-analysis
path: build/reports/dependency-check-report.html
-128
View File
@@ -1,128 +0,0 @@
name: PR Checks
on:
# Privilege escalation necessary to comment on pull requests from forks.
# 🚨 We must not check out or execute any code here, and be careful around use of user-controlled inputs.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [ opened, reopened, edited, synchronize, labeled, unlabeled ]
permissions: {}
jobs:
pr-checks:
# Upstream-only: this fork does not use the PR- label convention.
if: github.repository == 'element-hq/element-x-android'
name: PR Checks
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group: ${{ format('pr-checks-{0}', github.ref) }}
cancel-in-progress: true
permissions:
# Required to comment on the pull request.
pull-requests: write
steps:
- name: Check the pull request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
// Classes that must not be modified anymore. Add the file name suffix here to freeze a class.
const FROZEN_CLASSES = [
];
// Only this user is allowed to edit the translation files, since they come from Localazy.
const TRANSLATION_ALLOW_LIST = ["ElementBot"];
const pr = context.payload.pull_request;
const body = pr.body ?? "";
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
// Deleted files are not interesting for any of the checks below.
const paths = files.filter(file => file.status !== "removed").map(file => file.filename);
const warnings = [];
const failures = [];
if (!body) {
warnings.push("Please provide a description for this PR.");
}
if (paths.length > 50) {
warnings.push("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.");
}
if (pr.title.endsWith("…")) {
failures.push("Please provide a complete title that can be used as a changelog entry.");
}
if (pr.labels.filter(label => label.name.startsWith("PR-")).length !== 1) {
failures.push("Please add a single `PR-` label to categorise the changelog entry.");
}
for (const frozen of FROZEN_CLASSES) {
if (paths.some(path => path.endsWith(frozen))) {
failures.push(`Frozen class \`${frozen}\` has been modified. Please do not modify frozen class.`);
}
}
// Exclude the recorded screenshots, which are legitimately pngs.
if (paths.some(path => path.toLowerCase().endsWith(".png") && !path.includes("snapshots/images/"))) {
warnings.push("You seem to have made changes to some images. Please consider using a vector drawable.");
}
if (!TRANSLATION_ALLOW_LIST.includes(pr.user.login) && paths.some(path => path.endsWith("translations.xml"))) {
failures.push("Some translation files have been edited. Only user `ElementBot` (i.e. translations coming from Localazy) is allowed to do that. " +
"Please read more about translations management [in the doc](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#strings).");
}
warnings.forEach(warning => core.warning(warning));
failures.forEach(failure => core.error(failure));
// Keep a single comment up to date rather than adding a new one on every run.
const marker = "<!-- pr-checks -->";
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number
});
const existing = comments.find(comment => comment.body.includes(marker));
if (warnings.length || failures.length) {
const lines = [...failures.map(failure => `- ❌ ${failure}`),
...warnings.map(warning => `- ⚠️ ${warning}`)];
const commentBody = `${marker}\n### PR Checks\n\n${lines.join("\n")}`;
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: commentBody
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: commentBody
});
}
} else if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id
});
}
if (failures.length) {
core.setFailed(`${failures.length} check(s) failed, see the comment on the pull request.`);
}
+53 -191
View File
@@ -47,7 +47,7 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
@@ -65,11 +65,11 @@ jobs:
name: Search for invalid screenshot files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Search for invalid screenshot files
@@ -79,20 +79,20 @@ jobs:
name: Search for invalid dependencies
runs-on: [self-hosted, Linux, X64]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Search for invalid dependencies
@@ -107,7 +107,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-konsist-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-konsist-develop-{0}', github.sha) || format('check-konsist-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -122,19 +122,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: konsist-report
path: |
@@ -157,7 +157,7 @@ jobs:
cat /sys/fs/cgroup/memory.max 2>/dev/null \
|| cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null \
|| echo "no cgroup memory limit visible"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -172,12 +172,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests
@@ -191,7 +191,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-lint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-lint-develop-{0}', github.sha) || format('check-lint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -206,12 +206,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug
@@ -222,7 +222,7 @@ jobs:
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linting-report
path: |
@@ -236,7 +236,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-detekt-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-detekt-develop-{0}', github.sha) || format('check-detekt-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -251,19 +251,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon --continue
run: ./gradlew detekt $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: detekt-report
path: |
@@ -277,7 +277,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-ktlint-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-ktlint-develop-{0}', github.sha) || format('check-ktlint-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -292,19 +292,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES --continue
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ktlint-report
path: |
@@ -318,7 +318,7 @@ jobs:
group: ${{ github.ref == 'refs/heads/main' && format('check-docs-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-docs-develop-{0}', github.sha) || format('check-docs-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
@@ -342,7 +342,7 @@ jobs:
name: Check shell scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run shellcheck
@@ -360,175 +360,37 @@ jobs:
actions: read # Required for zizmor to read workflow files.
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
upload_reports:
name: Project Check Suite
runs-on: ubuntu-latest
needs: [konsist, lint, ktlint, detekt]
# Run even if a previous job failed: that is precisely when there are issues to report.
if: ${{ !cancelled() }}
permissions:
# Required to list the files modified by the pull request.
pull-requests: read
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Download reports from previous jobs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Annotate the lint, ktlint and detekt issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- name: Prepare Danger
if: always()
run: |
npm install --save-dev @babel/core
npm install --save-dev @babel/plugin-transform-flow-strip-types
yarn add danger-plugin-lint-report --dev
- name: Danger lint
if: always()
uses: danger/danger-js@58355dc2b274a87699c8b7deac01baba16d6f061 # 13.0.8
with:
script: |
// GitHub only surfaces a handful of annotations per level and per step, so cap what we emit
// and let the job summary carry the full list.
const MAX_ANNOTATIONS = 20;
const MAX_SUMMARY_ROWS = 200;
function unescapeXml(value) {
return value
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, "\"")
.replace(/&apos;/g, "'")
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
.replace(/&amp;/g, "&");
}
function attribute(tag, name) {
const match = new RegExp(`\\b${name}="([^"]*)"`).exec(tag);
return match ? unescapeXml(match[1]) : undefined;
}
// Paths in the reports are absolute paths on the runner which generated them, i.e.
// /home/runner/work/<repo>/<repo>/<path>.
const workspace = `/work/${context.repo.repo}/${context.repo.repo}/`;
function repoRelative(path) {
const index = path.indexOf(workspace);
return index === -1 ? path : path.slice(index + workspace.length);
}
// Checkstyle XML, as produced by ktlint and detekt:
// <file name="…"><error line=".." column=".." severity=".." message=".." source=".."/></file>
function parseCheckstyle(content) {
const issues = [];
const fileRegex = /<file\s+([^>]*?)(\/>|>([\s\S]*?)<\/file>)/g;
let file;
while ((file = fileRegex.exec(content)) !== null) {
const path = attribute(file[1], "name");
if (!path) continue;
const errorRegex = /<error\s+([^>]*?)\/?>/g;
let error;
while ((error = errorRegex.exec(file[3] ?? "")) !== null) {
issues.push({
file: repoRelative(path),
line: Number(attribute(error[1], "line")) || 1,
column: Number(attribute(error[1], "column")) || undefined,
severity: (attribute(error[1], "severity") ?? "warning").toLowerCase(),
rule: attribute(error[1], "source"),
message: attribute(error[1], "message") ?? "",
});
}
}
return issues;
}
// Android lint XML, which is not Checkstyle:
// <issue id=".." severity=".." message=".."><location file=".." line=".." column=".."/></issue>
function parseAndroidLint(content) {
const issues = [];
const issueRegex = /<issue\s+([^>]*?)(\/>|>([\s\S]*?)<\/issue>)/g;
let issue;
while ((issue = issueRegex.exec(content)) !== null) {
const location = /<location\s+([^>]*?)\/?>/.exec(issue[3] ?? "");
if (!location) continue;
const path = attribute(location[1], "file");
if (!path) continue;
issues.push({
file: repoRelative(path),
line: Number(attribute(location[1], "line")) || 1,
column: Number(attribute(location[1], "column")) || undefined,
severity: (attribute(issue[1], "severity") ?? "warning").toLowerCase(),
rule: attribute(issue[1], "id"),
message: attribute(issue[1], "message") ?? "",
});
}
return issues;
}
const reports = await (await glob.create("**/reports/**/*.xml")).glob();
core.info(`Found ${reports.length} XML report(s).`);
const fs = require("fs");
let issues = [];
for (const report of reports) {
const content = fs.readFileSync(report, "utf8");
if (content.includes("<checkstyle")) {
issues.push(...parseCheckstyle(content));
} else if (content.includes("<issues")) {
issues.push(...parseAndroidLint(content));
}
}
// Only report on the files touched by the pull request.
const pr = context.payload.pull_request;
if (pr) {
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number
});
const changed = new Set(files.map(file => file.filename));
issues = issues.filter(issue => changed.has(issue.file));
}
const seen = new Set();
issues = issues.filter(issue => {
const key = `${issue.file}:${issue.line}:${issue.rule}:${issue.message}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
if (issues.length === 0) {
core.info("No issue found in the files modified by this pull request.");
return;
}
const level = severity => {
if (severity === "error" || severity === "fatal") return "error";
if (severity === "warning") return "warning";
return "notice";
};
const rank = { error: 0, warning: 1, notice: 2 };
issues.sort((a, b) =>
rank[level(a.severity)] - rank[level(b.severity)] ||
a.file.localeCompare(b.file) ||
a.line - b.line
);
issues.slice(0, MAX_ANNOTATIONS).forEach(issue => {
const annotation = {
title: issue.rule,
file: issue.file,
startLine: issue.line,
startColumn: issue.column,
};
core[level(issue.severity)](issue.message, annotation);
});
core.summary.addHeading(`${issues.length} code quality issue(s)`, 3);
core.summary.addTable([
[{ data: "Severity", header: true }, { data: "File", header: true }, { data: "Rule", header: true }, { data: "Message", header: true }],
...issues.slice(0, MAX_SUMMARY_ROWS).map(issue => [
level(issue.severity),
`${issue.file}:${issue.line}`,
issue.rule ?? "",
issue.message,
]),
]);
if (issues.length > MAX_SUMMARY_ROWS) {
core.summary.addRaw(`…and ${issues.length - MAX_SUMMARY_ROWS} more, see the uploaded reports.`, true);
}
await core.summary.write();
args: "--dangerfile ./tools/danger/dangerfile-lint.js"
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
# Fallback for forks
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+6 -10
View File
@@ -60,29 +60,25 @@ jobs:
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0
with:
labels: Record-Screenshots
# Private fork: the action checks out and then runs `git lfs pull` on a cache miss. Upstream
# can do that anonymously because it is public; here the pull needs the job token, so the
# credentials must stay configured. Without this the step dies with
# `could not read Username for 'https://github.com'`.
- name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots'
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 zizmor: ignore[artipacked]
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: true
persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch'
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 zizmor: ignore[artipacked]
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: true
persist-credentials: false
- name: ☕️ Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
# Add gradle cache, this should speed up the process
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots
+46 -12
View File
@@ -34,18 +34,16 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check Matrix Rust SDK is not a nightly version
run: ./tools/release/check_no_nightly_sdk.sh
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -59,12 +57,50 @@ jobs:
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-app-gplay-bundle-unsigned
path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
enterprise:
name: Create App Bundle Enterprise
runs-on: ubuntu-latest
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
concurrency:
group: ${{ format('build-release-main-enterprise-{0}', github.sha) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Clone submodules
run: git submodule update --init --recursive
- name: Use JDK 21
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create Enterprise app bundle
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-enterprise-app-gplay-bundle-unsigned
path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab
fdroid:
name: Create APKs (FDroid)
runs-on: ubuntu-latest
@@ -86,18 +122,16 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Check Matrix Rust SDK is not a nightly version
run: ./tools/release/check_no_nightly_sdk.sh
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create APKs
env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -105,7 +139,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: elementx-app-fdroid-apks-unsigned
path: |
@@ -51,13 +51,18 @@ if [[ -z ${REPO} ]]; then
exit 1
fi
# Note: the previous screenshots are deliberately kept, so that Paparazzi can leave the ones that only
# differ by less than `maxPercentDifference` untouched instead of rewriting their Git LFS blob. The
# screenshots of previews that no longer exist are pruned afterwards by `pruneObsoleteSnapshots`, which
# `recordPaparazziDebug` is finalized by.
echo "Deleting previous screenshots"
./gradlew removeOldSnapshots --stacktrace --warn $GRADLE_ARGS
echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS
echo "Deleting previous screenshots"
./gradlew removeOldScreenshots --stacktrace --warn $GRADLE_ARGS
echo "Record screenshots (Compound)"
./gradlew :libraries:compound:recordRoborazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn $GRADLE_ARGS
echo "Committing changes"
git config http.sslVerify false
@@ -73,10 +78,6 @@ else
git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi
git add -A
if git diff --cached --quiet; then
echo "Screenshots are already up to date, nothing to commit."
exit 0
fi
git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
+3 -3
View File
@@ -52,19 +52,19 @@ jobs:
docker-images: true
swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with:
only-labels: "X-Needs-Info"
days-before-issue-stale: 30
@@ -1,85 +0,0 @@
name: Sync Enterprise submodule
on:
workflow_dispatch:
push:
branches: [ develop ]
paths: [ enterprise ]
schedule:
# At 06:00 UTC every day
- cron: '0 6 * * *'
permissions: {}
jobs:
sync-enterprise-submodule:
runs-on: ubuntu-latest
# Skip in forks
if: github.repository == 'element-hq/element-x-android'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Add SSH private keys for submodule repositories
uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0
with:
ssh-private-key: ${{ secrets.ELEMENT_ENTERPRISE_DEPLOY_KEY }}
- name: Check enterprise submodule reference
id: check
env:
EVENT_NAME: ${{ github.event_name }}
run: |
current=$(git ls-tree HEAD enterprise | awk '{print $3}')
# Do not init the submodule: the current commit may be on a deleted branch.
git init -q "$RUNNER_TEMP/enterprise"
git -C "$RUNNER_TEMP/enterprise" fetch -q git@github.com:element-hq/element-android-enterprise.git main
main=$(git -C "$RUNNER_TEMP/enterprise" rev-parse FETCH_HEAD)
if git -C "$RUNNER_TEMP/enterprise" merge-base --is-ancestor "$current" "$main" 2>/dev/null; then
on_main=true
else
on_main=false
fi
echo "Current: $current, main: $main, current is on main: $on_main"
if [ "$current" = "$main" ]; then
echo "Submodule is already up to date."
echo "update=false" >> "$GITHUB_OUTPUT"
elif [ "$EVENT_NAME" = "push" ] && [ "$on_main" = "true" ]; then
echo "Submodule targets a commit on main, nothing to do."
echo "update=false" >> "$GITHUB_OUTPUT"
else
echo "update=true" >> "$GITHUB_OUTPUT"
fi
{
echo "body<<EOF"
if [ "$on_main" = "true" ]; then
# Do not list the commit messages, the enterprise repository is private.
count=$(git -C "$RUNNER_TEMP/enterprise" rev-list --count "$current..$main")
echo "Update the \`enterprise\` submodule from \`$current\` to the latest commit of \`main\` (\`$main\`), $count new commit(s)."
else
echo "The \`enterprise\` submodule targets \`$current\`, which is not on the \`main\` branch."
echo
echo "Update it to the latest commit of \`main\` (\`$main\`)."
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"
if [ "$current" != "$main" ]; then
# Update the submodule reference without cloning it.
git update-index --cacheinfo "160000,$main,enterprise"
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -q -m "Update enterprise submodule to latest main" -m "Update to $main"
fi
- name: Create Pull Request to update the enterprise submodule
# On schedule and manual runs, also run when there is nothing to update, so that a stale PR gets closed.
if: ${{ steps.check.outputs.update == 'true' || github.event_name != 'push' }}
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
title: Update enterprise submodule to latest main
body: ${{ steps.check.outputs.body }}
branch: sync-enterprise-submodule
base: develop
delete-branch: true
labels: PR-Element-Pro
+4 -8
View File
@@ -7,29 +7,26 @@ on:
permissions: {}
env:
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
jobs:
sync-localazy:
runs-on: ubuntu-latest
# Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Setup Localazy
@@ -42,7 +39,6 @@ jobs:
./tools/localazy/downloadStrings.sh --all
./tools/localazy/importSupportedLocalesFromLocalazy.py
./tools/test/generateAllScreenshots.py
./tools/manifest/aaptDump.sh
- name: Create Pull Request for Strings
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
+2 -2
View File
@@ -14,11 +14,11 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
# No concurrency required, runs every time on a schedule.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.14
- name: Install Prerequisite dependencies
+10 -15
View File
@@ -29,7 +29,7 @@ env:
# JVMs against a 48 GiB cgroup limit, with no swap — the daemon gets OOM-killed and
# Gradle reports "the daemon has disappeared". GitHub's 4-core hosted runners reach only
# 2 x 2 x 2g = 16 GB and additionally have the 8 GB swapfile added above.
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4
jobs:
tests:
@@ -95,31 +95,25 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
run: git submodule update --init --recursive
- name: ☕️ Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit tests
run: ./gradlew :app:testGplayDebugUnitTest testDebugUnitTest -x :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --continue
- name: ⚙️ Run screenshot tests
run: ./gradlew verifyPaparazziDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: ⚙️ Generate kover coverage report
run: ./gradlew :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll -x testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES
- name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: 🚫 Upload kover failed coverage reports
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: kover-error-report
path: |
build/reports/kover
app/build/reports/kover
- name: ✅ Upload kover report (disabled)
if: always()
@@ -127,11 +121,12 @@ jobs:
- name: 🚫 Upload test results on error
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: tests-and-screenshot-tests-results
path: |
**/build/paparazzi/failures/
**/build/roborazzi/failures/
**/build/reports/tests/*UnitTest/
- name: 🚫 Modify summary on error
@@ -152,7 +147,7 @@ jobs:
if: env.CODECOV_TOKEN != ''
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+3
View File
@@ -31,6 +31,9 @@ __pycache__/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
-34
View File
@@ -1,39 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+1 -1
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;SPDX-License-Identifier: LicenseRef-Element-Commercial&#10;SPDX-License-Identifier: LicenseRef-Element-Proprietary" />
<option name="notice" value="© &amp;#36;today.year Element Creations Ltd.&#10;&#10;Element Creations Ltd, Element Software SARL, Element Software Inc.,&#10;and Element Software GmbH (the &quot;Element Group&quot;) only make this file available&#10;under a proprietary license model.&#10;&#10;Without a proprietary license with us, you cannot use this file. The terms of&#10;the proprietary license agreement between you and any member of the Element Group&#10;shall always apply to your use of this file. Unauthorised use, copying, distribution,&#10;or modification of this file, via any medium, is strictly prohibited.&#10;&#10;For details about the licensing terms, you must either visit our website or contact&#10;a member of our sales team." />
<option name="myName" value="Element Enterprise" />
</copyright>
</component>
+1
View File
@@ -16,6 +16,7 @@
<w>posthog</w>
<w>rageshake</w>
<w>securebackup</w>
<w>showkase</w>
<w>snackbar</w>
<w>spdx</w>
<w>swipeable</w>
+2 -2
View File
@@ -2,6 +2,6 @@
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.20" />
<option name="version" value="2.4.0" />
</component>
</project>
</project>
+21
View File
@@ -0,0 +1,21 @@
appId: ${MAESTRO_APP_ID}
---
- tapOn:
id: "login-change_server"
- takeScreenshot: build/maestro/200-ChangeServer
- tapOn: "matrix.org"
- tapOn:
id: "login-change_server"
- tapOn: "Other"
- tapOn:
id: "change_server-server"
- inputText: "element"
- hideKeyboard
- extendedWaitUntil:
visible: "element.io"
timeout: 10000
- tapOn: "element.io"
# Revert to matrix.org
- tapOn:
id: "login-change_server"
- tapOn: "matrix.org"
+2 -8
View File
@@ -3,17 +3,11 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn
# The account provider field defaults to matrix.org, so continue straight to the MAS login.
- runFlow: changeServer.yaml
- runFlow: ../assertions/assertLoginDisplayed.yaml
- tapOn:
id: "login-continue"
## MAS page
# Click on "Use another account" if MAS is already logged in:
- runFlow:
when:
visible: "Use another account"
commands:
- tapOn:
text: "Use another account"
- retry:
maxRetries: 3
commands:
+1 -10
View File
@@ -6,19 +6,10 @@ appId: ${MAESTRO_APP_ID}
- takeScreenshot: build/maestro/900-SignOutScreen
- back
- tapOn: "Remove this device"
# Ensure pressing back cancels
- back
- tapOn: "Remove this device"
# Ensure cancel cancels
- tapOn: "Cancel"
- tapOn: "Remove this device"
- tapOn:
id: "sign-out-submit"
# Cancel the dialog cancels the logout
- tapOn:
id: "dialog-negative"
- tapOn:
id: "sign-out-submit"
- tapOn: "Remove this device"
- tapOn:
id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml
+1 -1
View File
@@ -16,4 +16,4 @@ appId: ${MAESTRO_APP_ID}
- extendedWaitUntil:
visible: "Device verified"
timeout: 30000
- tapOn: "Done"
- tapOn: "Continue"
@@ -1,5 +1,5 @@
appId: ${MAESTRO_APP_ID}
---
- extendedWaitUntil:
visible: "Enter your account provider"
visible: "Change account provider"
timeout: 10000
@@ -1,21 +0,0 @@
appId: ${MAESTRO_APP_ID}
---
# Clean up the room list if it was left in a bad state by previous tests
- runFlow:
when:
visible: 'aRoomName'
commands:
- longPressOn: 'aRoomName'
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Leave"
- runFlow:
when:
visible: ${MAESTRO_INVITEE1_MXID}
commands:
- longPressOn: ${MAESTRO_INVITEE1_MXID}
- tapOn:
text: "Leave room"
index: 0
- tapOn: "Leave"
+1 -10
View File
@@ -7,16 +7,7 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- runFlow:
when:
visible: 'Start a chat with this new contact?'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Send invite?'
commands:
- tapOn: 'Send invite'
- tapOn: "Continue"
- takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2"
- scroll
@@ -8,12 +8,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Add description…"
- inputText: "aRoomTopic"
- tapOn: "Create"
# It can take ages to create room on matrix.org
- extendedWaitUntil:
timeout: 30000
visible: "Invite people"
- takeScreenshot: build/maestro/320-createAndDeleteRoom
- tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE1_MXID}
@@ -21,12 +15,6 @@ appId: ${MAESTRO_APP_ID}
text: ${MAESTRO_INVITEE1_MXID}
index: 1
- tapOn: "Finish"
- runFlow:
when:
visible: 'Invite new contact to this room?'
commands:
- tapOn:
id: "confirm_invite_unknown"
- tapOn: "aRoomName"
- tapOn: "Invite"
# assert there's 1 member and 1 invitee
@@ -35,10 +23,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn:
text: ${MAESTRO_INVITEE2_MXID}
index: 1
# Hide keyboard by tapping on an external element in case it's displayed, otherwise it will 'eat' the back button press
- tapOn:
# Navigation title
text: "Invite people"
- tapOn: "Invite"
- runFlow:
when:
@@ -46,6 +30,10 @@ appId: ${MAESTRO_APP_ID}
commands:
- tapOn:
id: "confirm_invite_unknown"
# Close the keyboard if it's still open
- tapOn: "Back"
# Go back to the room details screen
- tapOn: "Back"
- scrollUntilVisible:
direction: DOWN
element:
+1 -2
View File
@@ -1,7 +1,6 @@
appId: ${MAESTRO_APP_ID}
---
- runFlow: cleanUpRoomList.yaml
#- runFlow: searchRoomList.yaml
- runFlow: searchRoomList.yaml
- takeScreenshot: build/maestro/300-RoomList
- runFlow: timeline/timeline.yaml
- runFlow: roomContextMenu.yaml
@@ -3,5 +3,5 @@ appId: ${MAESTRO_APP_ID}
- takeScreenshot: build/maestro/520-Timeline
- tapOn: "Add attachment"
- tapOn: "Share location"
- tapOn: "Share my location"
- tapOn: "Share selected location"
- takeScreenshot: build/maestro/521-Timeline
+8 -61
View File
@@ -4,12 +4,9 @@ appId: ${MAESTRO_APP_ID}
id: "home_screen-settings"
- assertVisible: "Settings"
- takeScreenshot: build/maestro/600-Settings
## Account settings
- tapOn:
id: "settings-user_profile"
- assertVisible: "Edit profile"
- takeScreenshot: build/maestro/610-AccountEditProfile
text: "Analytics"
- assertVisible: "Share analytics data"
- back
- tapOn:
@@ -17,27 +14,16 @@ appId: ${MAESTRO_APP_ID}
- assertVisible: "Enable notifications on this device"
- back
- tapOn:
text: "Encryption"
- assertVisible: "Key storage"
- back
- tapOn:
text: "Moderation & safety"
- assertVisible: "Your appearance"
- back
- tapOn:
text: "Report a problem"
- assertVisible: "Report a problem"
- back
## App settings section
- assertVisible: "Appearance"
- tapOn:
text: "Media upload quality"
- assertVisible: "Media upload quality"
text: "About"
- assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back
- tapOn:
@@ -46,53 +32,14 @@ appId: ${MAESTRO_APP_ID}
- hideKeyboard
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "Location sharing"
- tapOn:
text: "Location sharing"
- assertVisible: "Live location updates"
text: "Advanced settings"
- assertVisible: "View source"
- back
## General section
- scrollUntilVisible:
direction: DOWN
element:
text: "Analytics"
- tapOn:
text: "Analytics"
- assertVisible: "Share analytics data"
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "Labs"
- tapOn:
text: "Labs"
- assertVisible: "Feeling experimental?"
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "About"
- tapOn:
text: "About"
- assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back
- scrollUntilVisible:
direction: DOWN
element:
text: "Developer options"
- tapOn:
text: "Developer options"
- assertVisible: "Feature flags"
- assertVisible: "View source"
- back
- back
+3 -5
View File
@@ -37,9 +37,8 @@ PRs must meet these rules.
- Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency.
- Use `PreviewParameterProvider` (e.g., `FooStatePreviewParam`) to provide states. Always add new states to existing providers at the end of the sequence.
- Use `PreviewParameterProvider` (e.g., `FooStateProvider`) to provide states.
- Wrap previews in `ElementPreview { ... }`.
- When writing tests, never try to record the screenshots, the CI will do it.
---
@@ -50,10 +49,9 @@ PRs must meet these rules.
- Title = changelog entry — descriptive, no "Fixes #…".
- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette).
- Screenshots/videos for visual changes.
- 500 additions max into production code. Test code can be larger — split large changes.
- 500 additions max — split large changes.
- Commits need a title and description; no tiny or massive commits.
- No history rewrites.
- When creating a pull request and if changes contain new or update(s) of the Composable Previews, add the label "Record-Screenshots" to the PR so that the CI will record the screenshots.
---
@@ -90,7 +88,7 @@ We use [Appyx](https://bumble-tech.github.io/appyx/) for navigation and [Molecul
| `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
| `FooState.kt` | Data class representing the immutable UI state. |
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. |
| `FooStatePreviewParam.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooStateProvider.kt` | Provides sample states for Previews and Screenshot tests. |
| `FooPresenterTest.kt` | Unit tests for the Presenter logic using Turbine. |
---
-634
View File
@@ -1,637 +1,3 @@
Changes in Element X v26.09.4
=============================
## Hotfix version:
Fixes an issue with the room list not displaying all activity from unread messages.
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.3...v26.09.4
Changes in Element X v26.09.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.09.3 -->
## What's Changed
### ✨ Features
* Integrate the new timeline renderer behind a feature flag by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7722
* Add headers (`h1-h6`) tag support for the new timeline renderer by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7754
### 🙌 Improvements
* Add smooth animation to voice messages play progress by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7698
* Add linkification to the new timeline renderer by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7731
### 🐛 Bugfixes
* Make global search use a `Joined` filter for room results by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7695
* Fix flaky test in `TimelinePresenterTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7702
* Close the threaded timelines created to create a poll, share a location or send media by @bmarty in https://github.com/element-hq/element-x-android/pull/7705
* Start sync manually after completing the auth flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7734
* Fix forwarding an event not working in threads by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7743
* Fix blank screen at launch when the homeserver expires the sliding sync session by @bmarty in https://github.com/element-hq/element-x-android/pull/7692
* Fix unread notification count in room list by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7749
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7699
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7736
### 🧱 Build
* Reduce parallel JVMs spawned for tests by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7701
* Replace git flow with regular git commands in the release script V2 by @bmarty in https://github.com/element-hq/element-x-android/pull/7707
* Remove Roborazzi, use Paparazzi for Compound by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7735
* Fix regex for invalid placeholders by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7738
* Add a script to install and launch an APK given a PR URL by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7752
### 🚧 In development 🚧
* New timeline event renderer: add new modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7721
* Add recent search results to global search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7742
### Dependency upgrades
* Update dependency io.sentry:sentry-android to v8.55.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7685
* Update coil to v3.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7693
* Update kotlinpoet to v2.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7709
* Update plugin sonarqube to v7.5.0.8588 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7671
* Update roborazzi to v1.74.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7712
* Update dependency io.element.android:element-call-embedded to v0.26.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7715
* Use patched Konsist version (forked, 0.18.0) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7719
* Update dependency net.zetetic:sqlcipher-android to v4.19.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7716
* Update metro to v1.4.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7711
* Update actions/setup-java action to v6.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7728
* Update dependency androidx.compose:compose-bom to v2026.09.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7730
* Update dependency androidx.sqlite:sqlite-ktx to v2.7.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7729
* Update dependencyAnalysis to v3.19.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7755
* Update media3 to v1.11.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7756
* Upgrade SDK bindings to `v26.09.26` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7780
### Others
* Let the user give a reason when removing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7538
* Iterate on client creation by @bmarty in https://github.com/element-hq/element-x-android/pull/7591
* Iteration on Acount provider by @bmarty in https://github.com/element-hq/element-x-android/pull/7703
* Do not report detekt LargeClass issues on test classes by @bmarty in https://github.com/element-hq/element-x-android/pull/7713
* Let the user give an optional reason when removing a message by @bmarty in https://github.com/element-hq/element-x-android/pull/7679
* Make NotificationResultProcessor.emit wait until the batch is processed by @Johennes in https://github.com/element-hq/element-x-android/pull/7640
* Settings iteration: move user status and update multi-account section by @bmarty in https://github.com/element-hq/element-x-android/pull/7741
* Update remove message confirmation dialog. by @bmarty in https://github.com/element-hq/element-x-android/pull/7757
* Add a script to end all the sessions of a Matrix account using MAS by @bmarty in https://github.com/element-hq/element-x-android/pull/7762
* Settings iteration: split account settings and application settings. by @bmarty in https://github.com/element-hq/element-x-android/pull/7750
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.2...v26.09.3
Changes in Element X v26.09.2
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.09.2 -->
## What's Changed
### 🙌 Improvements
* Keep the push foreground service alive while notifications are fetched by @Johennes in https://github.com/element-hq/element-x-android/pull/7639
* Stop clearing the remote config when clearing the cache by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7662
### 🐛 Bugfixes
* Fix fetched remote config being cached with the wrong key by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7626
* Make sure `enforce_element_pro` is taken into account in FOSS by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7631
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7628
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7657
### 🧱 Build
* Allow retrying finding the release assets in release script by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7619
* Improve detection of invalid placeholders in localazy strings by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7624
* Add `GRADLE_OPTS` for `sync-localazy.yml` flow by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7629
* Clean up ProGuard keep rules files by @bmarty in https://github.com/element-hq/element-x-android/pull/7647
* Replace Danger with actions/github-script by @bmarty in https://github.com/element-hq/element-x-android/pull/7649
* Remove Showkase and its in-app component browser by @bmarty in https://github.com/element-hq/element-x-android/pull/7678
* Improve Renovate configuration to fix issues with Maven Central by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7677
### Dependency upgrades
* Update dependency com.posthog:posthog-android to v3.60.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7616
* Update dependency org.jsoup:jsoup to v1.23.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7620
* Update dependency io.element.android:element-call-embedded to v0.25.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7622
* Update dependencyAnalysis to v3.19.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7623
* Update wysiwyg to v2.42.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7627
* Update android.gradle.plugin to v9.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7603
* Update actions/setup-java action to v6 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7614
* Update danger/danger-js action to v14 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7602
* Update camera to v1.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7632
* Update coil to v3.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7630
* Update dependency io.sentry:sentry-android to v8.54.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7636
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7650
* Bump SDK bindings to `v26.09.3` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7637
* Update zizmorcore/zizmor-action action to v0.6.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7658
* Update coil to v3.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7664
* Update dependency org.matrix.rustcomponents:sdk-android to v26.09.08 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7668
* Update dependency org.matrix.rustcomponents:sdk-android to v26.09.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7683
### 💼 Element Pro
* Fix remote config getting refreshed with `Origin.Cache` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7666
### Others
* Move knock and gallery to labs by @bmarty in https://github.com/element-hq/element-x-android/pull/7625
* Say so when this session has no secrets to verify another one with by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7551
* Expose the timeline item long press and swipe to reply as TalkBack actions by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7458
* Bump PR priority for our dependencies in Renovate by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7642
* Import string from Localazy and remove temporary file. by @bmarty in https://github.com/element-hq/element-x-android/pull/7645
* Revert "Say so when this session has no secrets to verify another one with" by @bmarty in https://github.com/element-hq/element-x-android/pull/7644
* Maestro (local): queue pending runs in the maestro-test concurrency group instead of cancelling them by @andreBurnt in https://github.com/element-hq/element-x-android/pull/7656
* Maestro (local): skip the lane on pull requests from forks by @andreBurnt in https://github.com/element-hq/element-x-android/pull/7655
* Update maplibre compose to 0.15 by @ganfra in https://github.com/element-hq/element-x-android/pull/7663
* Try improving renovate issues with Maven Central rate limiting by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7667
## New Contributors
* @andreBurnt made their first contribution in https://github.com/element-hq/element-x-android/pull/7656
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.09.1...v26.09.2
Changes in Element X v26.09.1
=============================
## What's Changed
### ✨ Features
* Support creating polls with multiple answers and send response with multiple answers by @bmarty in https://github.com/element-hq/element-x-android/pull/7613
### 🙌 Improvements
* Only track read receipts on message-like events by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7361
* Stop rescanning message bodies for emoji only on every recomposition by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7531
### 🐛 Bugfixes
* Ensure that noisy notifications are still noisy when the app has a PIN code. by @bmarty in https://github.com/element-hq/element-x-android/pull/7584
* Ensure that sessionVerifiedStatus is updated when the crypto is ready. by @bmarty in https://github.com/element-hq/element-x-android/pull/7604
* Fix invalid placeholders in translations by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7612
### 🧱 Build
* Do not open the editor when finishing the release by @bmarty in https://github.com/element-hq/element-x-android/pull/7583
* [release] Check that there is no unmerged PR with the label "Z-NextRelease", else exit by @bmarty in https://github.com/element-hq/element-x-android/pull/7606
### Dependency upgrades
* Update dependency net.zetetic:sqlcipher-android to v4.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7552
* Update dependency com.google.firebase:firebase-bom to v34.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7581
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7535
* Update dependency com.google.guava:guava to v33.7.1-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7534
* Update dependency org.unifiedpush.android:connector to v3.3.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7586
* Update dependency org.unifiedpush.android:connector to v3.3.5 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7595
* Update roborazzi to v1.73.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7618
### 💼 Element Pro
* Implement a hook allowing enterprise modules to perform actions on startup by @richvdh in https://github.com/element-hq/element-x-android/pull/7585
### Others
* Don't hide the thread of a deleted message inside the removed messages group by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7571
* Name a sender the timeline has no profile for from the room member list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7539
* Offer conversations in the system share sheet by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7574
* Rank the mention suggestions by how well they match what was typed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7536
* Log the SDK git Sha at application startup by @bmarty in https://github.com/element-hq/element-x-android/pull/7589
* Limit usage of nullable callback. by @bmarty in https://github.com/element-hq/element-x-android/pull/7588
* Fix crash due to JSON.stringify on a cirular structure in injectes js. by @toger5 in https://github.com/element-hq/element-x-android/pull/7600
* Support multi-select polls by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6477
* Remove ExternalSession, fallback login with Element Web is gone. by @bmarty in https://github.com/element-hq/element-x-android/pull/7608
* Fall back to the message body when the formatted body has nothing to render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7523
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.4...v26.09.1
Changes in Element X v26.08.4
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.4 -->
## What's Changed
### 🧱 Build
* Add releaseV2.sh, a release script automating the GitHub release by @bmarty in https://github.com/element-hq/element-x-android/pull/7579
* Update submodule ref. by @bmarty in https://github.com/element-hq/element-x-android/pull/7582
### Others
* Keep the formatting of a caption that is being edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7572
* Say when the server cannot be reached instead of showing nothing by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7567
* Keep saving files to Downloads once 32 files share the same name by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7344
* Fix removed messages hiding its thread on deletion by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7480
* Drop the sender's name from the room list preview of two-person rooms by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7562
* Document how the Element Call base URL developer option is used by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7546
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.3...v26.08.4
Changes in Element X v26.08.3
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.3 -->
## What's Changed
### ✨ Features
* Let users enter their full Matrix user ID on the account provider screen by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7500
* Enable user status feature and hide status text in the timeline by @bmarty in https://github.com/element-hq/element-x-android/pull/7517
### 🐛 Bugfixes
* Fix cold start with a mobile deep link by @bmarty in https://github.com/element-hq/element-x-android/pull/7488
* Make the account provider screen scrollable so its description is not clipped by the keyboard by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7521
### 🧱 Build
* Update Gradle Wrapper from 9.7.0 to 9.7.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7485
* Fix flaky test in `DefaultVoiceMessageComposerPresenterTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7487
* Fix warning about deprecated Gradle `keepRules.file` API by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7498
* Update Enterprise Copyright in idea settings, so that new files get an expected header. by @bmarty in https://github.com/element-hq/element-x-android/pull/7501
* Remove workaround for Renovate. by @bmarty in https://github.com/element-hq/element-x-android/pull/7502
### 🚧 In development 🚧
* Make PIP react to camera orientation changes in the other participant's device by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6951
* Feature: global search by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7410
### Dependency upgrades
* Update dependency org.maplibre.gl:android-sdk to v13.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7486
* Update dependency androidx.webkit:webkit to v1.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7484
* Update dependency io.sentry:sentry-android to v8.53.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7481
* Update dependency io.element.android:element-call-embedded to v0.24.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7474
* Update roborazzi to v1.72.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7489
* Update metro to v1.4.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7493
* Update dependency com.squareup.okhttp3:okhttp-bom to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7512
* Update dependency com.google.guava:guava to v33.7.0-android by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7516
* Update sdk to version 26.08.25 by @bmarty in https://github.com/element-hq/element-x-android/pull/7550
### 💼 Element Pro
* Add `ClientEnterpriseHook` and `ClientBuilderEnterpriseHook` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7495
* Remove dependency on enterprise module. by @bmarty in https://github.com/element-hq/element-x-android/pull/7505
### Others
* Show the right image when opening one that has not finished sending by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7359
* Move strings to the login module by @bmarty in https://github.com/element-hq/element-x-android/pull/7494
* Allow empty problem description if the GitHub issue number is present for rageshakes by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7497
* Apply the system theme when switching back to it from a forced light or dark theme by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7453
* Show dialog with reason when user clicks on a message with a failure by @bmarty in https://github.com/element-hq/element-x-android/pull/7490
* Keep a focused timeline focused until the event has been rendered by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7463
* Do not render in-chat key verification requests in the timeline by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7443
* Share a file whose mime type is not one of the supported ones by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7435
* Show reactions in the pinned messages list by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6676
* Send a concrete mime type for media shared with a wildcard one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7449
* Rename module and ensure that naming convention is followed by @bmarty in https://github.com/element-hq/element-x-android/pull/7514
* Fix lint issue, ensure that PiP methods are not called on old devices not supporting them. by @bmarty in https://github.com/element-hq/element-x-android/pull/7515
* Rename PreviewParameterProvider subclasses to the PreviewParam suffix by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7468
* Update Ref. by @bmarty in https://github.com/element-hq/element-x-android/pull/7518
* Remove the reference to the decommissioned OIDC playground by @sandhose in https://github.com/element-hq/element-x-android/pull/7519
* Hide the encryption badge when key storage is set up on the server by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7447
* Rename remaining events sealed interface by @bmarty in https://github.com/element-hq/element-x-android/pull/7533
* Stop waiting on a verification request the other session never accepts by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7457
* Point the proxy debugging doc at the method that builds the clients by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7543
* Show the build date in the screenshot gallery footer by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7544
* Run the database migrations again when a database is upgraded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7570
* Update fonts on the headings by @bmarty in https://github.com/element-hq/element-x-android/pull/7520
* Add a "Push rules" entry in the developer options by @bmarty in https://github.com/element-hq/element-x-android/pull/7577
## New Contributors
* @sandhose made their first contribution in https://github.com/element-hq/element-x-android/pull/7519
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.2...v26.08.3
Changes in Element X v26.08.2
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.2 -->
**Note: this release in an internal release, it will not be pushed to the stores.**
## What's Changed
### ✨ Features
* Remove the legacy web-based account creation for matrix.org by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7412
### 🙌 Improvements
* Move the custom tile server URL config to the Element wellknown content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7416
* Explain a rate limiting push gateway in the notification troubleshooter by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7448
* Move the `:libraries:wellknown` logic into `enterprise` modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7429
* Refresh the room members when a mention is started by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7438
* Close the invite screen once the invite has been declined by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7440
* Use Profiles sliding sync extension check for own profile subscription by @ganfra in https://github.com/element-hq/element-x-android/pull/7403
* Simplify choosing the account provider during sign in, with autocomplete of previously used ones by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7394
### 🐛 Bugfixes
* Fix wrong TextComposer placeholder displayed when in a thread. by @bmarty in https://github.com/element-hq/element-x-android/pull/7413
* Fix bug reports failing to send after the app has crashed by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7395
* Fix outgoing session verification stuck with incomplete recovery setup by @hardcore-sushi in https://github.com/element-hq/element-x-android/pull/7433
* Keep transparency when an image is shared into the app with an imprecise mime type by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7452
* Don't restart the sync service if the client is shutting down by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7472
* Normalize the applicationName used in user agent by @bmarty in https://github.com/element-hq/element-x-android/pull/7477
* Keep playing a voice message when another app plays a short sound by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7454
* Keep the send button when the composer is emptied while editing a message by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7456
* Do not open the room again when the app is resumed from the recents list by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7436
* Stop waiting for a room alias that the remote server never resolves by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7446
### 🧱 Build
* build-rust-sdk: print the command we run by @richvdh in https://github.com/element-hq/element-x-android/pull/7407
* Remove license check on Pro modules. by @bmarty in https://github.com/element-hq/element-x-android/pull/7418
* Improve build time config and update the Firebase configuration by @bmarty in https://github.com/element-hq/element-x-android/pull/7473
* Make the PUSHER_APP_ID configurable at build time. by @bmarty in https://github.com/element-hq/element-x-android/pull/7478
### 📄 Documentation
* Add KDoc to all the interfaces of the api modules by @bmarty in https://github.com/element-hq/element-x-android/pull/7426
### 🚧 In development 🚧
* [LinkNewDevice] Ensure the notice screen is also protected by the timeout. by @bmarty in https://github.com/element-hq/element-x-android/pull/7428
### Dependency upgrades
* Update kotlin to v2.3.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7396
* Update plugin sonarqube to v7.4.0.8496 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7415
* Update metro to v1.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7421
* Update roborazzi to v1.71.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7423
* Update dependency io.sentry:sentry-android to v8.52.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7414
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7420
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7431
* Update dependency org.maplibre.compose:maplibre-compose to v0.14.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7432
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.17 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7460
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.19 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7479
* Update media3 to v1.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7427
* Update dependency androidx.compose:compose-bom to v2026.08.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7483
### 💼 Element Pro
* Add hooks to allow providing additional feature flags in EX pro by @richvdh in https://github.com/element-hq/element-x-android/pull/7419
* Use the right base URL for the ESS config/well known endpoints by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7469
### Others
* Do not display pinned events that the client cannot render by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7379
* Hide the forward action on live location shares by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7343
* Do not fail the screenshot recording job when there is nothing to commit by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7355
* Show the account a UnifiedPush registration belongs to in the distributor by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7345
* Stop repeating messages that are already shown in a room notification by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7370
* Turn a matrix: URI in a message into a pill by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7354
* Send a message with Enter on a physical keyboard, Shift+Enter for a new line by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7353
* Do not group timeline messages sent more than five minutes apart by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7349
* Linkify URLs whose host is a bracketed IPv6 address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7347
* Add missing tests for `RustTemporaryMatrixClient` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7417
* Revert "Keep manually revealed media visible when re-entering a room" by @bmarty in https://github.com/element-hq/element-x-android/pull/7424
* Fix outdated documentation. by @bmarty in https://github.com/element-hq/element-x-android/pull/7430
* Sync strings - New language supported: Arabic by @bmarty in https://github.com/element-hq/element-x-android/pull/7461
* Keep the focused login field visible above the keyboard by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7455
* Relax image compression so photos and screenshots keep more detail by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7451
* Open the user profile of a user id that contains a slash by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7445
* Remove the F-Droid dex patching step from the release script by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7442
* Convert markdown to HTML when a caption is edited by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7441
* Exclude carriage returns from the login and password fields by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7439
* Show an error when the room to leave cannot be found by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7437
* Give the room name in the timeline top bar a 48dp tap target by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7450
* Model the fake voice recorder on a cancellable recording job by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7466
## New Contributors
* @hardcore-sushi made their first contribution in https://github.com/element-hq/element-x-android/pull/7433
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.1...v26.08.2
Changes in Element X v26.08.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.1 -->
## What's Changed
### 🙌 Improvements
* Render push notifications for live location share starts (MSC4505) by @ara4n in https://github.com/element-hq/element-x-android/pull/7203
* [Task] Show active call participants in the people list #7199 by @Eklavya47 in https://github.com/element-hq/element-x-android/pull/7300
### 🐛 Bugfixes
* Fix overlap issue. by @bmarty in https://github.com/element-hq/element-x-android/pull/7331
* Announce checked/unchecked state for user rows in invite screen by @bmarty in https://github.com/element-hq/element-x-android/pull/7136
* Display 'add reactions' menu in the threads screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7388
* Expand the emoji picker to full height when the keyboard is shown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7382
* UI fixes by @bmarty in https://github.com/element-hq/element-x-android/pull/7409
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7389
### 🧱 Build
* Don't check enterprise previews in `KonsistPreviewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7297
* CI: split unit tests, screenshots and coverage in `tests.yml` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7296
* Update Gradle Wrapper from 9.6.1 to 9.7.0 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7335
### 🚧 In development 🚧
* QR code login iteration by @bmarty in https://github.com/element-hq/element-x-android/pull/6719
* User status last pieces by @ganfra in https://github.com/element-hq/element-x-android/pull/7328
* [Link new device] Add timeout by @bmarty in https://github.com/element-hq/element-x-android/pull/7338
### Dependency upgrades
* Update dependency org.maplibre.compose:maplibre-compose to v0.13.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7291
* Update android.gradle.plugin to v9.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7248
* Update dependency io.sentry:sentry-android to v8.50.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7298
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7306
* Update android.gradle.plugin to v9.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7304
* Update dependency com.posthog:posthog-android to v3.56.4 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7311
* Update dependencyAnalysis to v3.18.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7318
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7317
* Update dependency io.github.zxing-cpp:android to v3.1.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7322
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.05 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7324
* Update dependency androidx.constraintlayout:constraintlayout to v2.2.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7329
* Update reactivecircus/android-emulator-runner action to v2.38.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7189
* Update dependency io.sentry:sentry-android to v8.51.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7301
* Update dependency org.maplibre.gl:android-sdk to v13.4.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7305
* Update dependency io.element.android:element-call-embedded to v0.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7323
* Update metro to v1.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7334
* Update dependency com.google.firebase:firebase-bom to v34.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7333
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7175
* Update dependency org.jsoup:jsoup to v1.23.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7332
* Update dependency androidx.constraintlayout:constraintlayout-compose to v1.1.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7330
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7383
* Update actions/setup-java action to v5.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7339
* Update gradle/actions action to v6.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7384
* Update plugin dependencycheck to v13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7386
* Update dependency org.matrix.rustcomponents:sdk-android to v26.08.11 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7402
### 💼 Element Pro
* Add support for checking the ESS config in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7302
### Others
* Add extra vertical padding to `ReplyToInvalidContent` and `ReplyToNotFoundContent` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7288
* Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7249
* Bump enterprise module to the latest commit by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7307
* Bump enterprise module to the latest commit by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7309
* Move deviceId and git details to developer settings by @bmarty in https://github.com/element-hq/element-x-android/pull/7241
* Show a Message not found placeholder when a replied to message cannot be loaded by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7376
* Update in reply to message when message is not found. by @bmarty in https://github.com/element-hq/element-x-android/pull/7387
* Add default `OAUTH_CLIENT_URL_PATH` alternative by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7385
* Fix the feature module template writing the impl build script into the api module by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7374
* Let users enter their full Matrix user ID when searching for a homeserver by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7375
* Keep manually revealed media visible when re-entering a room by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7378
* Copy a blocked user's Matrix ID by long pressing them by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7372
* Do not draw the media loading progress bar under the status bar by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7366
* Do not notify about an event sent by another account on the same device by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7360
* Keep the opened image on screen when the media gallery fails to load by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7362
* Do not linkify the server part of a fediverse handle as an email address by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7356
* Expose global account data get and set on MatrixClient by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7373
* Show the alias or the direct message user's Matrix ID in the room picker by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7371
* Use a dedicated string for the Mute room notification mode by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7367
* Keep playing a voice message when the screen turns off by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7365
* Let a voice message without a waveform be seeked by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7363
* Ignore a second voice recording start while one is already running by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7346
* Do not offer to start a call in a DM with no other member by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7352
* Do not notify about a redaction whose target event is unknown by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7350
* Ignore a room list filter that is incompatible with an already selected one by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7342
* Stop a right to left sender name from reordering a room list preview by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7348
* Do not append the application MapTiler key to a custom map style url by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7340
* Hide space children you have left that can only be joined by invitation by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7369
* Never re-encode a video at a higher bitrate than the source by @hayaksi1 in https://github.com/element-hq/element-x-android/pull/7351
* Display push notifications for knocks by @ara4n in https://github.com/element-hq/element-x-android/pull/7207
## New Contributors
* @hayaksi1 made their first contribution in https://github.com/element-hq/element-x-android/pull/7249
* @ara4n made their first contribution in https://github.com/element-hq/element-x-android/pull/7203
* @Eklavya47 made their first contribution in https://github.com/element-hq/element-x-android/pull/7300
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.08.0...v26.08.1
Changes in Element X v26.08.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.08.0 -->
## What's Changed
### ✨ Features
* feat(call): Active call timeline rendering by @BillCarsonFr in https://github.com/element-hq/element-x-android/pull/7051
### 🙌 Improvements
* Announce expanded/collapsed state on the timeline group header by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7226
* Allow adding extra elements to the developer settings from external modules by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7230
* Update keyboard visibility to track hasFocus by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7284
### 🐛 Bugfixes
* Live location sharing timeout by @ganfra in https://github.com/element-hq/element-x-android/pull/7184
* Fix logging via QR code when using MDM-configured homeserver by @mxandreas in https://github.com/element-hq/element-x-android/pull/7228
* Fix #7181: Remove back arrow from device verified screen and use 'Done' text by @thirumani-vihaan in https://github.com/element-hq/element-x-android/pull/7222
* Fix padding for captions of media in 'not found' state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7247
* Fix banned mime type errors in `DefaultContentScannerService` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7255
* Fix `ContentErrorView` not calculating its text size synchronously by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7281
* Fix media viewer for medias not related to events displaying invalid state by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7280
* Try making `HeaderFooterView` more robust by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7267
* Fix insets in several screens by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7285
### ⚠️ API Changes
* Add a password strength estimator wrapping the SDK zxcvbn API by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/7186
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7277
### 🧱 Build
* CI: fix 'Update Gradle Wrapper' by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7238
* Update Gradle Wrapper from 9.5.1 to 9.6.1 by @ElementBot in https://github.com/element-hq/element-x-android/pull/7240
* Localazy: improve search, parallelize checks by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7261
* Run `Konsist` tests sequentially by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7293
### 📄 Documentation
* Group Element Pro changes into a dedicated release notes section by @manuroe in https://github.com/element-hq/element-x-android/pull/7236
### 🚧 In development 🚧
* Plug emoji picker for custom user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7257
* WIP : display user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7232
### Dependency upgrades
* Update roborazzi to v1.67.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7188
* Update dependency io.sentry:sentry-android to v8.48.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7217
* Update dependency com.google.crypto.tink:tink-android to v1.23.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7229
* Update kotlin to v2.3.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7234
* Update dependency net.zetetic:sqlcipher-android to v4.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7218
* Update dependency com.google.firebase:firebase-bom to v34.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7237
* Update dependencyAnalysis to v3.16.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7212
* Update actions/setup-java action to v5.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7214
* Update dependency io.element.android:element-call-embedded to v0.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7161
* Update kotlin to v2.4.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7244
* Update roborazzi to v1.68.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7245
* Update metro to v1.3.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7243
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7258
* Update zizmorcore/zizmor-action action to v0.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7256
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.23 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7262
* Update dependency io.sentry:sentry-android to v8.49.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7263
* Update dependencyAnalysis to v3.17.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7265
* Update actions/setup-java action to v5.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7268
* Update dependency org.jetbrains.kotlinx:kover-gradle-plugin to v0.9.9 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7273
* Update actions/setup-python action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7278
* Update roborazzi to v1.70.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7274
* Update actions/checkout action to v7.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7282
* Import Compound tokens from `v10.2.3` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7272
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.28 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7287
* Update dependency org.maplibre.gl:android-sdk to v13.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7290
### 💼 Element Pro
* Content scanner (Element Pro): scan timeline events for unsafe content by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7159
* Add content scanner support to media gallery and viewer (Element Pro) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7233
* Enforce HS disabling E2EE in DM creation too (Element Pro) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7292
* Allow adding a path suffix to the OAuth client URI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7286
### Others
* [A11y] Improve rendering on focused UI items by @bmarty in https://github.com/element-hq/element-x-android/pull/7182
* Add a button role to clickable icons and text on the login/onboarding screens by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7225
* Expose formatting toggle state to TalkBack by @unclejay80 in https://github.com/element-hq/element-x-android/pull/7227
* Add warning to the 'Scan QR code' screen by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7264
* Refactor SearchBarResultState.Initial/NoResultsFound to data objects by @ganfra in https://github.com/element-hq/element-x-android/pull/7266
* Remove no longer temporary strings by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7260
* Use the right insets in the home screen UI by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7271
* Make settings section separators visible in black theme by @mvanhorn in https://github.com/element-hq/element-x-android/pull/7039
## New Contributors
* @thirumani-vihaan made their first contribution in https://github.com/element-hq/element-x-android/pull/7222
* @mvanhorn made their first contribution in https://github.com/element-hq/element-x-android/pull/7039
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.1...v26.08.0
Changes in Element X v26.07.1
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.07.1 -->
## What's Changed
### ✨ Features
* Scroll to unread messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7024
### 🙌 Improvements
* Collapse runs of deleted messages in the timeline by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7195
* Allow homeserver to disable encryption in Element Pro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7198
### 🐛 Bugfixes
* Fix jump-to-unread requiring two taps for an out-of-window read marker by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6943
* Add content description to video preview for screen readers by @bmarty in https://github.com/element-hq/element-x-android/pull/7137
* Fix accessibility for message composer input and read receipts button by @bmarty in https://github.com/element-hq/element-x-android/pull/7135
* Add accessibility label to search input by @bmarty in https://github.com/element-hq/element-x-android/pull/7133
* Fix timeline reading order distorted when TalkBack is active by @bmarty in https://github.com/element-hq/element-x-android/pull/7138
* Add programmatic label and focus indicator to PIN entry field by @bmarty in https://github.com/element-hq/element-x-android/pull/7132
* Fix timelines not loading after marking a room as read from notification when app was killed by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7202
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7192
### 🧱 Build
* Bump the default Gradle daemon heap to match what CI already uses by @PolynomialDivision in https://github.com/element-hq/element-x-android/pull/7152
* Update screenshots by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7155
* Update GitHub templates by @bmarty in https://github.com/element-hq/element-x-android/pull/7170
* Add check on merged manifest by @bmarty in https://github.com/element-hq/element-x-android/pull/7172
* Fix AAPT dump missing and reordering some permissions by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7193
* Try fixing DM creation in Maestro by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7204
### 🚧 In development 🚧
* Support gallery messages by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6519
* Support gallery messages by @bmarty in https://github.com/element-hq/element-x-android/pull/7112
* Initial content scanner integration by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7139
* WIP : Set user status by @ganfra in https://github.com/element-hq/element-x-android/pull/7094
### Dependency upgrades
* Update dependency io.sentry:sentry-android to v8.44.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7093
* Update dependency org.maplibre.gl:android-sdk to v13.3.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7129
* Update actions/setup-java action to v5.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7143
* Update danger/danger-js action to v13.0.10 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7142
* Update dependency io.sentry:sentry-android to v8.46.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7140
* Update dependencyAnalysis to v3.16.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7144
* Update plugin gms_google_services to v4.5.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7086
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha21 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7007
* Update dependency androidx.compose.material3:material3 to v1.5.0-alpha22 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7158
* Update metro to v1.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7168
* Update dependency io.sentry:sentry-android to v8.47.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7178
* Update dependency androidx.compose:compose-bom to v2026.06.01 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7174
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.5.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7180
* Update dependency io.github.sergio-sastre.ComposablePreviewScanner:android to v0.9.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7201
* Update dependency io.github.zxing-cpp:android to v3.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7206
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7194
* Update dependency androidx.sqlite:sqlite-ktx to v2.7.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7177
* Update dependency org.matrix.rustcomponents:sdk-android to v26.07.15 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7216
### Others
* Timeline: jump-to-unread FAB and mark-as-read shortcut by @jennaharris7 in https://github.com/element-hq/element-x-android/pull/6694
* Iterate on visual list by @bmarty in https://github.com/element-hq/element-x-android/pull/7141
* Fix wrong avatar shown in the thread list's top bar by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7044
* Update onboarding screen text style by @bmarty in https://github.com/element-hq/element-x-android/pull/7157
* Add full SVG support by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7020
* Add skin tone picker to the emoji picker by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6917
* Fix the message edit being lost when attaching media by @manfrommedan in https://github.com/element-hq/element-x-android/pull/7090
* Cleanup AndroidManifest.xml files. by @bmarty in https://github.com/element-hq/element-x-android/pull/7171
* Handle local network permission - Android 17 by @ganfra in https://github.com/element-hq/element-x-android/pull/7165
* Add item count text as the placeholder for in reply to box for galleries by @bxdxnn in https://github.com/element-hq/element-x-android/pull/7176
* Default to dark theme for EC by @toger5 in https://github.com/element-hq/element-x-android/pull/7162
## New Contributors
* @PolynomialDivision made their first contribution in https://github.com/element-hq/element-x-android/pull/7152
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.07.0...v26.07.1
Changes in Element X v26.07.0
=============================
<!-- Release notes generated using configuration in .github/release.yml at v26.07.0 -->
## What's Changed
### ✨ Features
* Add read all messages feature by @hghgrtut in https://github.com/element-hq/element-x-android/pull/7049
### 🐛 Bugfixes
* Detect RTL text in events and adjust the text rendering by @jmartinesp in https://github.com/element-hq/element-x-android/pull/6994
### 🗣 Translations
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7071
* Sync Strings by @ElementBot in https://github.com/element-hq/element-x-android/pull/7115
### 🧱 Build
* Fix Sonar with AGP 9 (some workarounds) by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7055
* Disable periodically generating the screenshot gallery by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7110
* Try a different workaround for flaky `ScanQrCodeViewTest` by @jmartinesp in https://github.com/element-hq/element-x-android/pull/7109
### 🚧 In development 🚧
* Protect link new device by @bmarty in https://github.com/element-hq/element-x-android/pull/6992
### Dependency upgrades
* Update metro to v1.2.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7057
* Update gradle/actions action to v6.2.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7063
* Update roborazzi to v1.64.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7068
* Update rnkdsh/action-upload-diawi action to v1.5.13 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7067
* Update dependency io.element.android:element-call-embedded to v0.20.2 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7075
* Update dependency com.github.matrix-org:matrix-analytics-events to v0.37.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7079
* Update actions/setup-java action to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7088
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.18 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7052
* Update firebaseAppDistribution to v5.3.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7096
* Update dependency com.google.firebase:firebase-bom to v34.15.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7089
* Update dependency com.google.crypto.tink:tink-android to v1.22.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7105
* Update actions/checkout action to v7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7098
* Update lifecycle to v2.11.0 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7097
* Update danger/danger-js action to v13.0.8 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7104
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.25 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7107
* Update dependency androidx.compose:compose-bom to v2026.06.00 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7095
* Update dependency io.element.android:element-call-embedded to v0.20.3 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7117
* Update zizmorcore/zizmor-action action to v0.5.7 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7116
* Update dependency org.matrix.rustcomponents:sdk-android to v26.06.30 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7127
* Update dependency com.posthog:posthog-android to v3.51.1 by @renovate[bot] in https://github.com/element-hq/element-x-android/pull/7128
### Others
* Fix small code smell by @bmarty in https://github.com/element-hq/element-x-android/pull/7058
* Hide message notification content when the app is locked by @bxdxnn in https://github.com/element-hq/element-x-android/pull/6902
## New Contributors
* @hghgrtut made their first contribution in https://github.com/element-hq/element-x-android/pull/7049
**Full Changelog**: https://github.com/element-hq/element-x-android/compare/v26.06.4...v26.07.0
Changes in Element X v26.06.4
=============================
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem 'danger'
-12
View File
@@ -6,18 +6,6 @@ top section matching the released version — the heading must have the exact
form `## Changes in <version> (<date>)` and the whole section must stay under
500 characters. See `.claude/skills/releasing-healthchat/SKILL.md`.
## Changes in 26.10.0 (2026-10-20)
🎉 Neue Funktionen
- Nachrichtensuche: Chats durchsuchen, auch in verschlüsselten Räumen.
- Umfragen mit Mehrfachauswahl.
🙌 Verbesserungen
- Basis aktualisiert auf Element X 26.10.0 mit zahlreichen Fehlerbehebungen.
- Einstellungen neu gegliedert in Konto- und App-Einstellungen.
## Changes in 26.08.0 (2026-08-06)
🎉 Neue Funktionen
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph.
* Equivalent to the following declaration:
*
* ```
* @BindingContainer
* @ContributesTo(Scope::class)
* interface YourNodeBindingContainer {
* @Binds
* @IntoMap
* @NodeKey(YourNode::class)
* fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*>
* }
* ```
* abstract class YourNodeModule {
* @Binds
* @IntoMap
* @NodeKey(YourNode::class)
* abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
*}
*/
@Target(AnnotationTarget.CLASS)
annotation class ContributesNode(
+30 -45
View File
@@ -21,7 +21,6 @@ import extension.allFeaturesImpl
import extension.allLibrariesImpl
import extension.allServicesImpl
import extension.buildConfigFieldStr
import extension.environmentOrGradleValue
import extension.locales
import extension.setupDependencyInjection
import extension.testCommonDependencies
@@ -89,14 +88,13 @@ android {
storePassword = "android"
}
register("nightly") {
keyAlias = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_KEYID", "signing.element.nightly.keyId")
keyPassword = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD", "signing.element.nightly.keyPassword")
storeFile = file(
environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_STOREFILE", "signing.element.nightly.storeFile")
?.ifBlank { null }
?: "./signature/nightly.keystore"
)
storePassword = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD", "signing.element.nightly.storePassword")
keyAlias = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYID")
?: project.property("signing.element.nightly.keyId") as? String?
keyPassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD")
?: project.property("signing.element.nightly.keyPassword") as? String?
storeFile = file("./signature/nightly.keystore")
storePassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD")
?: project.property("signing.element.nightly.storePassword") as? String?
}
// HealthChat Play Store upload key. The keystore is not in the repo:
// CI decodes it from the RELEASE_KEYSTORE_BASE64 secret (see
@@ -147,11 +145,24 @@ android {
optimization {
enable = true
keepRules {
// Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value).
includeDefault = true
files.add(File(projectDir, "common-proguard-rules.pro"))
files.add(getDefaultProguardFile("proguard-android-optimize.txt"))
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardFile = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
File(projectDir, "enterprise-proguard-rules.pro")
} else {
// These default rules prevent the OSS app from being obfuscated
File(projectDir, "default-proguard-rules.pro")
}
if (flavorProguardFile.exists()) {
files.add(flavorProguardFile)
} else {
logger.warn("Proguard file ${flavorProguardFile.absolutePath} does not exist")
}
}
// Our custom keep rules are registered as `keepRules` source folders in the `androidComponents` block below,
// as the former `keepRules.files` DSL is deprecated since AGP 9.
}
}
@@ -236,30 +247,6 @@ androidComponents {
)
onVariants { variant ->
// Register the R8 keep rules source folders for optimized build types (release, nightly).
// Replaces the deprecated `optimization.keepRules.files` DSL (AGP 9+).
if (variant.buildType != "debug") {
variant.sources.keepRules?.let { keepRules ->
// Common rules, always applied.
keepRules.addStaticSourceDirectory("proguard/common")
// Depending on whether the app flavor is enterprise or not we want to use different proguard rules.
val flavorProguardDir = if (isEnterpriseBuild) {
// Custom rules for enterprise builds
"../enterprise/proguard"
} else {
// Custom fules for FOSS builds
"proguard/foss"
}
if (File(projectDir, flavorProguardDir).exists()) {
keepRules.addStaticSourceDirectory(flavorProguardDir)
} else {
logger.warn("Proguard folder ${File(projectDir, flavorProguardDir).absolutePath} does not exist")
}
}
}
// Assigns a different version code for each output APK
// other than the universal APK.
variant.outputs.forEach { output ->
@@ -298,6 +285,7 @@ dependencies {
implementation(projects.features.migration.api)
implementation(projects.appnav)
implementation(projects.appconfig)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.compose)
if (ModulesConfig.pushProvidersConfig.includeFirebase) {
@@ -307,21 +295,22 @@ dependencies {
implementation(projects.libraries.pushproviders.unifiedpush)
}
// Google Play Services fused location backend. Discovered at runtime via ServiceLoader by the
// location feature.
"gplayImplementation"(libs.maplibre.compose.location.runtime.gms)
implementation(libs.appyx.core)
implementation(libs.androidx.splash)
implementation(libs.androidx.core)
implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.startup)
implementation(libs.androidx.preference)
implementation(libs.coil)
implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp.logging)
implementation(libs.serialization.json)
implementation(libs.matrix.emojibase.bindings)
testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test)
@@ -343,7 +332,6 @@ licensee {
allow("BSD-3-Clause")
allow("EPL-1.0")
allowUrl("https://opensource.org/license/bsd-3-clause")
allowUrl("https://opensource.org/license/bsd-2-clause")
allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/")
@@ -354,9 +342,6 @@ licensee {
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android")
// The rustls-platform-verifier dependency does not provide a license file in their maven repo, but there are MIT and Apache licenses in
// the source code repo: see https://github.com/rustls/rustls-platform-verifier
ignoreDependencies(groupId = "org.rustls", artifactId = "rustls-platform-verifier")
}
fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
+2
View File
@@ -0,0 +1,2 @@
# Don't obfuscate anything for non-enterprise builds
-dontobfuscate
@@ -1,9 +0,0 @@
# Don't obfuscate anything for non-enterprise builds
-dontobfuscate
# Strip PostHog's Firebase push token integration. We don't use it and it breaks F-Droid.
# Removing the contents of this method will mark the Firebase-related classes as unused,
# which will allow R8 to remove them from the final build.
-assumevalues class com.posthog.android.PostHogAndroidConfig {
boolean getCapturePushNotificationSubscriptions() return false;
}
+4 -8
View File
@@ -8,6 +8,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- To be able to install APK from the application -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".ElementXApplication"
android:allowBackup="false"
@@ -53,10 +56,9 @@
-->
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@@ -126,12 +128,6 @@
<data android:host="user" />
<data android:host="room" />
</intent-filter>
<!--
Conversation shortcuts offered in the Direct Share row of the system share sheet.
-->
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
<!-- Incoming share simple -->
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -9,19 +9,14 @@
package io.element.android.x
import android.app.Application
import android.os.StrictMode
import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.startup.AppInitializer
import androidx.work.Configuration
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.createGraphFactory
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.SdkMetadata
import io.element.android.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.x.di.AppGraph
import io.element.android.x.di.ApplicationBindings
import io.element.android.x.info.logApplicationInfo
import io.element.android.x.initializer.CacheCleanerInitializer
import io.element.android.x.initializer.CrashInitializer
@@ -34,37 +29,19 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config
.setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
.build()
@Inject lateinit var sdkMetadata: SdkMetadata
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate() {
super.onCreate()
enableStrictMode()
AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java)
initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java)
}
bindings<ApplicationBindings>().inject(this)
logApplicationInfo(this, sdkMetadata.sdkGitSha)
logApplicationInfo(this)
// Disable the strict offset check for anchored draggable components, as it can cause issues with bottom sheets.
// Remove once https://issuetracker.google.com/issues/477038695 is fixed.
isAnchoredDraggableComponentsStrictOffsetCheckEnabled = false
}
private fun enableStrictMode() {
// Workaround to avoid NetworkOnMainThreadException when using the network on the main thread.
// This is a temporary solution to rustls doing the certificate verification using blocking network requests.
// It should be removed once the underlying issue is fixed.
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
@@ -61,26 +61,6 @@ class MainActivity : NodeActivity() {
setContent {
MainContent(appBindings)
}
val activity = this
appBindings.appStartupHooks().forEach {
lifecycleScope.launch {
it.onAppStartup(activity)
}
}
}
/**
* The pre-Android 12 way into picture-in-picture for a native call.
*
* Everything else about it is installed where the call is drawn, which knows whose call it is.
* This cannot be: an Activity override has no session in scope. From Android 12 the system is told
* up front that this Activity would like to shrink, and the call handles it without ever coming
* through here.
*/
override fun onUserLeaveHint() {
super.onUserLeaveHint()
appBindings.nativeCallPip().onUserLeaveHint(this)
}
@Composable
@@ -10,13 +10,10 @@ package io.element.android.x.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Multibinds
import io.element.android.features.callnative.api.NativeCallPip
import io.element.android.features.enterprise.api.AppStartupHook
import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.migration.api.MigrationEntryPoint
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -53,15 +50,5 @@ interface AppBindings {
fun buildMeta(): BuildMeta
/**
* For picture-in-picture. Only an Activity is told it is being left, and only the call knows
* whether that is worth shrinking for, so `MainActivity` needs this much of the native call and
* no more.
*/
fun nativeCallPip(): NativeCallPip
fun sentrySdkDsn(): SentrySdkDsn?
@Multibinds(allowEmpty = true)
fun appStartupHooks(): Set<AppStartupHook>
}
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig
import io.element.android.features.enterprise.api.IsEnterpriseBuild
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
@@ -28,6 +28,8 @@ import io.element.android.libraries.di.BaseDirectory
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.recentemojis.api.EmojibaseProvider
import io.element.android.libraries.recentemojis.impl.DefaultEmojibaseProvider
import io.element.android.x.BuildConfig
import io.element.android.x.R
import kotlinx.coroutines.CoroutineName
@@ -38,7 +40,7 @@ import java.io.File
@BindingContainer
@ContributesTo(AppScope::class)
object AppBindingContainer {
object AppModule {
@Provides
@BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File {
@@ -74,18 +76,17 @@ object AppBindingContainer {
fun providesBuildMeta(
@ApplicationContext context: Context,
buildType: BuildType,
isEnterpriseBuild: IsEnterpriseBuild,
enterpriseService: EnterpriseService,
): BuildMeta {
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
val isEnterprise = isEnterpriseBuild()
return BuildMeta(
isDebuggable = BuildConfig.DEBUG,
buildType = buildType,
applicationName = applicationName,
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
applicationId = BuildConfig.APPLICATION_ID,
isEnterpriseBuild = isEnterprise,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME,
@@ -111,7 +112,13 @@ object AppBindingContainer {
@Provides
@SingleIn(AppScope::class)
fun providesSnackbarDispatcher(): SnackbarDispatcher {
fun provideSnackbarDispatcher(): SnackbarDispatcher {
return SnackbarDispatcher()
}
@Provides
@SingleIn(AppScope::class)
fun providesEmojibaseProvider(@ApplicationContext context: Context): EmojibaseProvider {
return DefaultEmojibaseProvider(context)
}
}
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.x.di
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo
import io.element.android.x.ElementXApplication
@ContributesTo(AppScope::class)
interface ApplicationBindings {
fun inject(application: ElementXApplication)
}
@@ -15,7 +15,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
@ContributesBinding(AppScope::class)
class DefaultSessionGraphFactory(
private val appGraph: AppGraph,
private val appGraph: AppGraph
) : SessionGraphFactory {
override fun create(client: MatrixClient): Any {
return appGraph.sessionGraphFactory.create(client)
@@ -10,22 +10,32 @@ package io.element.android.x.info
import android.content.Context
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.log.logger.wrapInBox
import io.element.android.x.BuildConfig
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun logApplicationInfo(context: Context, sdkGitSha: String) {
val appVersion = "${BuildConfig.VERSION_NAME} (${context.getVersionCodeFromManifest()}) - ${BuildConfig.BUILD_TYPE} / ${BuildConfig.FLAVOR}"
fun logApplicationInfo(context: Context) {
val appVersion = buildString {
append(BuildConfig.VERSION_NAME)
append(" (")
append(context.getVersionCodeFromManifest())
append(") - ")
append(BuildConfig.BUILD_TYPE)
append(" / ")
append(BuildConfig.FLAVOR)
}
// TODO Get SDK version somehow
val sdkVersion = "SDK VERSION (TODO)"
val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
listOf(
" Application version: $appVersion",
" Element X: https://github.com/element-hq/element-x-android/commit/${BuildConfig.GIT_REVISION}",
" SDK : https://github.com/matrix-org/matrix-rust-sdk/commit/$sdkGitSha",
" Local time: $date",
)
.wrapInBox(minBoxInsideWidth = 80)
.forEach { Timber.d(it) }
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------")
Timber.d(" Application version: $appVersion")
Timber.d(" Git SHA: ${BuildConfig.GIT_REVISION}")
Timber.d(" SDK version: $sdkVersion")
Timber.d(" Local time: $date")
Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------\n\n\n\n")
}
@@ -8,14 +8,11 @@
package io.element.android.x.initializer
import android.annotation.SuppressLint
import android.content.Context
import android.system.Os
import android.util.Log
import androidx.startup.Initializer
import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.x.di.AppBindings
@@ -27,15 +24,6 @@ private const val ELEMENT_X_TARGET = "elementx"
class PlatformInitializer : Initializer<Unit> {
override fun create(context: Context) {
// Init the SDK with the Android Context, so it can be used with rustls and other Android specific features
runCatchingExceptions {
org.matrix.rustsdk.Android.init(context)
}.onFailure {
// We can't log this to Timber since the logs aren't initialized yet, so we log it to Logcat instead
@SuppressLint("LogNotTimber")
Log.e("PlatformInitializer", "Failed to init the SDK with an Android Context. The app probably won't work properly.", it)
}
val appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService()
val platformService = appBindings.platformService()
@@ -53,8 +41,6 @@ class PlatformInitializer : Initializer<Unit> {
sdkSentryDsn = appBindings.sentrySdkDsn()?.value?.takeIf { it.isNotBlank() },
)
bugReporter.setCurrentTracingLogLevel(logLevel.name)
// Init the tracing service with the configuration so we have logging enabled as soon as possible
platformService.init(tracingConfiguration)
// Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true)
-1
View File
@@ -1,6 +1,5 @@
<!-- File generated by importSupportedLocalesFromLocalazy.py, do not edit -->
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar"/>
<locale android:name="be"/>
<locale android:name="bg"/>
<locale android:name="ca"/>
@@ -30,384 +30,5 @@
<domain includeSubdomains="true">home</domain>
<domain includeSubdomains="true">lan</domain>
<domain includeSubdomains="true">localdomain</domain>
<!-- Domains below are imported from rustls-platform-verifier 0.2.0 by tools/dependencies/update_rustls_platform_verifier.py -->
<!-- They are needed to fetch the certificate revocation lists over cleartext -->
<domain includeSubdomains="true">abitabdv.crl.certum.pl</domain>
<domain includeSubdomains="true">abitabev.crl.certum.pl</domain>
<domain includeSubdomains="true">abitabov.crl.certum.pl</domain>
<domain includeSubdomains="true">alpiro.crl.sectigo.com</domain>
<domain includeSubdomains="true">anticdv.crl.certum.pl</domain>
<domain includeSubdomains="true">antrustovsslg2r30ca.crl.certum.pl</domain>
<domain includeSubdomains="true">autontrustprodvtlsg2r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">bitcert.crl.sectigo.com</domain>
<domain includeSubdomains="true">btdvtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">c.cf-b.ssl.com</domain>
<domain includeSubdomains="true">c.cf-i.ssl.com</domain>
<domain includeSubdomains="true">c.pki.goog</domain>
<domain includeSubdomains="true">ca1.agid.gov.it</domain>
<domain includeSubdomains="true">cdn.d-trust-cloudcrl.net</domain>
<domain includeSubdomains="true">cdp.disig.sk</domain>
<domain includeSubdomains="true">cdp.elektronicznypodpis.pl</domain>
<domain includeSubdomains="true">cdp.geotrust.com</domain>
<domain includeSubdomains="true">cdp.rapidssl.com</domain>
<domain includeSubdomains="true">cdp.thawte.com</domain>
<domain includeSubdomains="true">cdp1.disig.sk</domain>
<domain includeSubdomains="true">cdp1.pca.dfn.de</domain>
<domain includeSubdomains="true">cdpa.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpb.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpc.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpd.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpe.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpf.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpg.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdph.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">cdpi.digitalcertvalidation.com</domain>
<domain includeSubdomains="true">certcloud.crl.trust-provider.com</domain>
<domain includeSubdomains="true">certdata.crl.sectigo.com</domain>
<domain includeSubdomains="true">certera.crl.sectigo.com</domain>
<domain includeSubdomains="true">certificates.godaddy.com</domain>
<domain includeSubdomains="true">certum-crl.wt.trustasia.com</domain>
<domain includeSubdomains="true">certum.crl.sheca.com</domain>
<domain includeSubdomains="true">certumdvtlsg2e39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumdvtlsg2r39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumevtlsg2e39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumevtlsg2r39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumovtlsg2e39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">certumovtlsg2r39ca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcadveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcadvrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaeveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaevrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaoveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">cfcaovrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">cnssldvtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">cnsslovtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">crl-01.r1.ica.amazontrust.com</domain>
<domain includeSubdomains="true">crl-01.r1.ica.amznts.eu</domain>
<domain includeSubdomains="true">crl-c.emsign.com</domain>
<domain includeSubdomains="true">crl-pro.litessl.com</domain>
<domain includeSubdomains="true">crl.anf.es</domain>
<domain includeSubdomains="true">crl.apple.com</domain>
<domain includeSubdomains="true">crl.businessid.telesec.de</domain>
<domain includeSubdomains="true">crl.buypass.no</domain>
<domain includeSubdomains="true">crl.catrust.oemssl.cn</domain>
<domain includeSubdomains="true">crl.certigna.fr</domain>
<domain includeSubdomains="true">crl.certsign.ro</domain>
<domain includeSubdomains="true">crl.certum.pl</domain>
<domain includeSubdomains="true">crl.cfca.com.cn</domain>
<domain includeSubdomains="true">crl.cntrus.oemssl.cn</domain>
<domain includeSubdomains="true">crl.cnwebtrust.oemssl.cn</domain>
<domain includeSubdomains="true">crl.comodoca.com</domain>
<domain includeSubdomains="true">crl.crlocsp.cn</domain>
<domain includeSubdomains="true">crl.cybertrust.ne.jp</domain>
<domain includeSubdomains="true">crl.d-trust.net</domain>
<domain includeSubdomains="true">crl.dhimyotis.com</domain>
<domain includeSubdomains="true">crl.digicert.cn</domain>
<domain includeSubdomains="true">crl.digicert.eu</domain>
<domain includeSubdomains="true">crl.e-szigno.hu</domain>
<domain includeSubdomains="true">crl.e2m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m01.eu.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s14.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s15.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s16.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s17.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s18.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s19.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s20.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e2s21.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m01.eu.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s22.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s23.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s24.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s25.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s26.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s27.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s28.amazontrust.com</domain>
<domain includeSubdomains="true">crl.e3s29.amazontrust.com</domain>
<domain includeSubdomains="true">crl.emsign.com</domain>
<domain includeSubdomains="true">crl.ensuredca.com</domain>
<domain includeSubdomains="true">crl.firmaprofesional.com</domain>
<domain includeSubdomains="true">crl.gdca.com.cn</domain>
<domain includeSubdomains="true">crl.global.sheca.com</domain>
<domain includeSubdomains="true">crl.globalsign.com</domain>
<domain includeSubdomains="true">crl.globalsign.net</domain>
<domain includeSubdomains="true">crl.godaddy.com</domain>
<domain includeSubdomains="true">crl.harica.gr</domain>
<domain includeSubdomains="true">crl.izenpe.com</domain>
<domain includeSubdomains="true">crl.litessl.com</domain>
<domain includeSubdomains="true">crl.netsolssl.com</domain>
<domain includeSubdomains="true">crl.oem.trustca.net</domain>
<domain includeSubdomains="true">crl.pcsp.telesec.de</domain>
<domain includeSubdomains="true">crl.pki.goog</domain>
<domain includeSubdomains="true">crl.quovadisglobal.com</domain>
<domain includeSubdomains="true">crl.r1001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m01.eu.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s18.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s19.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s20.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s21.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s22.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s23.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s24.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r2s25.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r3001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4001.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m01.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m02.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m03.amazontrust.com</domain>
<domain includeSubdomains="true">crl.r4m04.amazontrust.com</domain>
<domain includeSubdomains="true">crl.root-e1.certainly.com</domain>
<domain includeSubdomains="true">crl.root-r1.certainly.com</domain>
<domain includeSubdomains="true">crl.rootca1.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rootca2.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rootca3.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rootca4.amazontrust.com</domain>
<domain includeSubdomains="true">crl.rztrust.oemssl.cn</domain>
<domain includeSubdomains="true">crl.sbca.telesec.de</domain>
<domain includeSubdomains="true">crl.sectigo.com</domain>
<domain includeSubdomains="true">crl.sectigochina.com</domain>
<domain includeSubdomains="true">crl.serverid.telesec.de</domain>
<domain includeSubdomains="true">crl.starfieldtech.com</domain>
<domain includeSubdomains="true">crl.swisssign.ch</domain>
<domain includeSubdomains="true">crl.trust-provider.cn</domain>
<domain includeSubdomains="true">crl.trust-provider.com</domain>
<domain includeSubdomains="true">crl.tuntrust.tn</domain>
<domain includeSubdomains="true">crl.usertrust.com</domain>
<domain includeSubdomains="true">crl.wt.trustasia.com</domain>
<domain includeSubdomains="true">crl.xinchacha.oemssl.cn</domain>
<domain includeSubdomains="true">crl.zw.oemssl.cn</domain>
<domain includeSubdomains="true">crl05.actalis.it</domain>
<domain includeSubdomains="true">crl06.actalis.it</domain>
<domain includeSubdomains="true">crl09.actalis.it</domain>
<domain includeSubdomains="true">crl1.ecert.gov.hk</domain>
<domain includeSubdomains="true">crl13.actalis.it</domain>
<domain includeSubdomains="true">crl14.actalis.it</domain>
<domain includeSubdomains="true">crl15.actalis.it</domain>
<domain includeSubdomains="true">crl2.microsoft.com</domain>
<domain includeSubdomains="true">crl3.digicert.com</domain>
<domain includeSubdomains="true">crl4.digicert.com</domain>
<domain includeSubdomains="true">crls.certainly.com</domain>
<domain includeSubdomains="true">crls.ssl.com</domain>
<domain includeSubdomains="true">cyberfolks2.crl.certum.pl</domain>
<domain includeSubdomains="true">depo.kamusm.gov.tr</domain>
<domain includeSubdomains="true">digitaltrust.crl.sectigo.com</domain>
<domain includeSubdomains="true">dnencrypt.crl.sectigo.com</domain>
<domain includeSubdomains="true">dvtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">dvtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">e5.c.lencr.org</domain>
<domain includeSubdomains="true">e6.c.lencr.org</domain>
<domain includeSubdomains="true">e7.c.lencr.org</domain>
<domain includeSubdomains="true">e8.c.lencr.org</domain>
<domain includeSubdomains="true">ec2ca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2ca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2ca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2sslca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2sslca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec2sslca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3ca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3ca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3ca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3sslca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3sslca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ec3sslca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eca.hinet.net</domain>
<domain includeSubdomains="true">edvtlsca2023-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">edvtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">edvtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">emudhra.crl.sectigo.com</domain>
<domain includeSubdomains="true">eosslca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eosslca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eosslca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eovtlsca2023-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eovtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eovtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eqca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqcpca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqcpca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqcpca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqoca2017-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqoca2017-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqoca2017-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2018-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2018-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2018-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2023-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">eqtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">esafer.crl.sectigo.com</domain>
<domain includeSubdomains="true">eue2m1.crl.root.amznts.eu</domain>
<domain includeSubdomains="true">eue3m1.crl.root.amznts.eu</domain>
<domain includeSubdomains="true">eur2m1.crl.root.amznts.eu</domain>
<domain includeSubdomains="true">eveccca.crl.certum.pl</domain>
<domain includeSubdomains="true">evrsaca.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcadv.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcaev.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcaov.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcatrustauthdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcatrustauthevtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">gdcatrustauthovtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">geant.crl.sectigo.com</domain>
<domain includeSubdomains="true">genious.crl.sectigo.com</domain>
<domain includeSubdomains="true">geossldvtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">geosslevtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">geosslovtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">globaltrust.crl.sectigo.com</domain>
<domain includeSubdomains="true">globessl.crl.sectigo.com</domain>
<domain includeSubdomains="true">gogetssldv.crl.certum.pl</domain>
<domain includeSubdomains="true">gogetsslev.crl.certum.pl</domain>
<domain includeSubdomains="true">gogetsslov.crl.certum.pl</domain>
<domain includeSubdomains="true">homepldvtlsg2r35ca.crl.certum.pl</domain>
<domain includeSubdomains="true">homepldvtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">homeplovtlsg2r35ca.crl.certum.pl</domain>
<domain includeSubdomains="true">homeplovtlsg2r36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">httpcrl.trust.telia.com</domain>
<domain includeSubdomains="true">ica.navercloudtrust.com</domain>
<domain includeSubdomains="true">ica.navercorp.com</domain>
<domain includeSubdomains="true">issauth.crl.sectigo.com</domain>
<domain includeSubdomains="true">itrusdv2.crl.certum.pl</domain>
<domain includeSubdomains="true">itrusdvtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">itrusov2.crl.certum.pl</domain>
<domain includeSubdomains="true">itrusovtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">itso.crl.sectigo.com</domain>
<domain includeSubdomains="true">joyssldvtlsg2r33ca.crl.certum.pl</domain>
<domain includeSubdomains="true">joysslovtlsg2r33ca.crl.certum.pl</domain>
<domain includeSubdomains="true">kingnettechdv.crl.certum.pl</domain>
<domain includeSubdomains="true">kingnettechev.crl.certum.pl</domain>
<domain includeSubdomains="true">kingnettechov.crl.certum.pl</domain>
<domain includeSubdomains="true">kr.crl.digicert.com</domain>
<domain includeSubdomains="true">lh2.crl.certum.pl</domain>
<domain includeSubdomains="true">namecheap.crl.sectigo.com</domain>
<domain includeSubdomains="true">nazwassldvtlsg2e29ca.crl.certum.pl</domain>
<domain includeSubdomains="true">nazwassldvtlsg2r29ca.crl.certum.pl</domain>
<domain includeSubdomains="true">netartcadv2.crl.certum.pl</domain>
<domain includeSubdomains="true">netartssldvtlsg2e31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">nijimo.crl.sectigo.com</domain>
<domain includeSubdomains="true">nyalabscadv.crl.certum.pl</domain>
<domain includeSubdomains="true">nyatworkdv.crl.certum.pl</domain>
<domain includeSubdomains="true">omitsecurity.crl.sectigo.com</domain>
<domain includeSubdomains="true">onesign.crl.sectigo.com</domain>
<domain includeSubdomains="true">osslca2016-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">osslca2016-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">osslca2016-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">ovtlsca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">ovtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">pki-crl.atos.net</domain>
<domain includeSubdomains="true">pki.telesec.de</domain>
<domain includeSubdomains="true">pkipro.certsign.ro</domain>
<domain includeSubdomains="true">positiwise.crl.sectigo.com</domain>
<domain includeSubdomains="true">psw.crl.sectigo.com</domain>
<domain includeSubdomains="true">public.wisekey.com</domain>
<domain includeSubdomains="true">qiduodv.crl.certum.pl</domain>
<domain includeSubdomains="true">qtlsca2018-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">qtlsca2018-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">qtlsca2018-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">qtlsca2026-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">r10.c.lencr.org</domain>
<domain includeSubdomains="true">r11.c.lencr.org</domain>
<domain includeSubdomains="true">r12.c.lencr.org</domain>
<domain includeSubdomains="true">r13.c.lencr.org</domain>
<domain includeSubdomains="true">rca.navercloudtrust.com</domain>
<domain includeSubdomains="true">rca.navercorp.com</domain>
<domain includeSubdomains="true">repo.pubcert.jprs.jp</domain>
<domain includeSubdomains="true">repo1.secomtrust.net</domain>
<domain includeSubdomains="true">repository.secomtrust.net</domain>
<domain includeSubdomains="true">repository.tls.hinet.net</domain>
<domain includeSubdomains="true">rootca.twca.com.tw</domain>
<domain includeSubdomains="true">rootglobaldv.crl.certum.pl</domain>
<domain includeSubdomains="true">rootnetworksdv2.crl.certum.pl</domain>
<domain includeSubdomains="true">shenzhendv.crl.certum.pl</domain>
<domain includeSubdomains="true">shenzhenev.crl.certum.pl</domain>
<domain includeSubdomains="true">shenzhenov.crl.certum.pl</domain>
<domain includeSubdomains="true">shoperdvtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">shuididv.crl.certum.pl</domain>
<domain includeSubdomains="true">shuidiev.crl.certum.pl</domain>
<domain includeSubdomains="true">shuidiov.crl.certum.pl</domain>
<domain includeSubdomains="true">soomadv.crl.certum.pl</domain>
<domain includeSubdomains="true">ssl2ca2016-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">ssl2ca2016-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">ssl2ca2016-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">sslca2014-crl1.e-szigno.hu</domain>
<domain includeSubdomains="true">sslca2014-crl2.e-szigno.hu</domain>
<domain includeSubdomains="true">sslca2014-crl3.e-szigno.hu</domain>
<domain includeSubdomains="true">sslcom.crl.certum.pl</domain>
<domain includeSubdomains="true">ssllimiteddv.crl.certum.pl</domain>
<domain includeSubdomains="true">sslserver.twca.com.tw</domain>
<domain includeSubdomains="true">subca.crl.certum.pl</domain>
<domain includeSubdomains="true">titrust.crl.sectigo.com</domain>
<domain includeSubdomains="true">tlcdvtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">tlcevtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">tlcovtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">tlsrootca2025-crl.e-szigno.hu</domain>
<domain includeSubdomains="true">trustasia.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiadv.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiadvtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaev.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaevtlse35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaevtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaov.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaovtlse35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustasiaovtlsr35g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">trustocean.crl.certum.pl</domain>
<domain includeSubdomains="true">tstlser20.crl.telesec.de</domain>
<domain includeSubdomains="true">tstlsrr23.crl.telesec.de</domain>
<domain includeSubdomains="true">unitedtrustov.crl.certum.pl</domain>
<domain includeSubdomains="true">validation.identrust.com</domain>
<domain includeSubdomains="true">verokey.crl.sectigo.com</domain>
<domain includeSubdomains="true">vtrusdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">vtrusovtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">webnic.crl.sectigo.com</domain>
<domain includeSubdomains="true">whitessldvtlsg2e36ca.crl.certum.pl</domain>
<domain includeSubdomains="true">wosign.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus-dvca.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus-evca.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus-ovca.crl.certum.pl</domain>
<domain includeSubdomains="true">wotrus.crl.sectigo.com</domain>
<domain includeSubdomains="true">wtca-cafiles.itrus.com.cn</domain>
<domain includeSubdomains="true">wtca-crl.itrus.com.cn</domain>
<domain includeSubdomains="true">www.accv.es</domain>
<domain includeSubdomains="true">www.cert.fnmt.es</domain>
<domain includeSubdomains="true">www.d-trust.net</domain>
<domain includeSubdomains="true">www.microsoft.com</domain>
<domain includeSubdomains="true">x1.c.lencr.org</domain>
<domain includeSubdomains="true">x2.c.lencr.org</domain>
<domain includeSubdomains="true">xcctrustdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xcctrustevtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xcctrustovtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchacha2dv.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchacha2ov.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchachatrustdvtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchachatrustevtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinchachatrustovtlsg2r34ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinnetdvtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinnetevtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">xinnetovtlsr34g2ca.crl.certum.pl</domain>
<domain includeSubdomains="true">ye.c.lencr.org</domain>
<domain includeSubdomains="true">ye1.c.lencr.org</domain>
<domain includeSubdomains="true">ye2.c.lencr.org</domain>
<domain includeSubdomains="true">yektadv.crl.certum.pl</domain>
<domain includeSubdomains="true">yektaov.crl.certum.pl</domain>
<domain includeSubdomains="true">yr.c.lencr.org</domain>
<domain includeSubdomains="true">yr1.c.lencr.org</domain>
<domain includeSubdomains="true">yr2.c.lencr.org</domain>
<domain includeSubdomains="true">zerossl.crl.sectigo.com</domain>
<domain includeSubdomains="true">ziwit.crl.sectigo.com</domain>
<domain includeSubdomains="true">zycatrustdvtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">zycatrustevtlsg3r31ca.crl.certum.pl</domain>
<domain includeSubdomains="true">zycatrustovtlsg3r31ca.crl.certum.pl</domain>
</domain-config>
</network-security-config>
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- Ref: https://developer.android.com/training/articles/security-config.html -->
<!-- By default, do not allow clearText traffic -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
<!-- Allow clearText traffic on some specified host -->
<domain-config cleartextTrafficPermitted="true">
<!-- Localhost -->
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<!-- Localhost for Android emulator -->
<domain includeSubdomains="true">10.0.2.2</domain>
<!-- Onion services -->
<domain includeSubdomains="true">onion</domain>
<!-- Domains that are used for LANs -->
<!-- These are IANA recognized special use domain names, see https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtml -->
<domain includeSubdomains="true">home.arpa</domain>
<domain includeSubdomains="true">local</domain> <!-- Note this has been reserved for use with mDNS -->
<domain includeSubdomains="true">test</domain>
<!-- These are observed in the wild either by convention or RFCs that have not been accepted, and are not currently TLDs -->
<domain includeSubdomains="true">home</domain>
<domain includeSubdomains="true">lan</domain>
<domain includeSubdomains="true">localdomain</domain>
</domain-config>
</network-security-config>
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
Please see LICENSE files in the repository root for full details.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Lets the system offer the conversation shortcuts published by
DefaultNotificationConversationService in the Direct Share row of the share sheet.
The category must match SHARE_TARGET_CATEGORY in that class.
-->
<share-target android:targetClass="io.element.android.x.MainActivity">
<data android:mimeType="*/*" />
<category android:name="io.element.android.category.SHARE_TARGET" />
</share-target>
</shortcuts>
+11 -4
View File
@@ -22,13 +22,19 @@ android {
defaultConfig {
buildConfigFieldStr(
name = "URL_POLICY",
// HealthChat fork: a configured value wins for every build type, not only enterprise ones.
value = BuildTimeConfig.URL_POLICY ?: if (isEnterpriseBuild) "" else "https://element.io/cookie-policy",
value = if (isEnterpriseBuild) {
BuildTimeConfig.URL_POLICY ?: ""
} else {
"https://element.io/cookie-policy"
},
)
buildConfigFieldStr(
name = "BUG_REPORT_URL",
// HealthChat fork: a configured value wins for every build type, not only enterprise ones.
value = BuildTimeConfig.BUG_REPORT_URL ?: if (isEnterpriseBuild) "" else "https://rageshakes.element.io/api/submit",
value = if (isEnterpriseBuild) {
BuildTimeConfig.BUG_REPORT_URL ?: ""
} else {
"https://rageshakes.element.io/api/submit"
},
)
buildConfigFieldStr(
name = "BUG_REPORT_APP_NAME",
@@ -42,6 +48,7 @@ android {
}
dependencies {
implementation(libs.coroutines.core)
implementation(libs.androidx.annotationjvm)
implementation(libs.androidx.corektx)
implementation(projects.libraries.matrix.api)
@@ -9,8 +9,15 @@
package io.element.android.appconfig
object AuthenticationConfig {
const val MATRIX_ORG_URL = "https://matrix.org"
/**
* URL with some docs that explain what's sliding sync and how to add it to your home server.
*/
const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md"
/**
* Force a sliding sync proxy url, if not null, the proxy url in the .well-known file will be ignored.
*/
val SLIDING_SYNC_PROXY_URL: String? = null
}
@@ -9,10 +9,9 @@
package io.element.android.appconfig
object LearnMoreConfig {
// HealthChat fork: every "learn more" link points at the HealthChat site instead of element.io/help.
const val ENCRYPTION_URL: String = "https://www.healthchat.ch"
const val DEVICE_VERIFICATION_URL: String = "https://www.healthchat.ch"
const val SECURE_BACKUP_URL: String = "https://www.healthchat.ch"
const val IDENTITY_CHANGE_URL: String = "https://www.healthchat.ch"
const val HISTORY_VISIBLE_URL: String = "https://www.healthchat.ch"
const val ENCRYPTION_URL: String = "https://element.io/help#encryption"
const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
const val HISTORY_VISIBLE_URL: String = "https://element.io/en/help#e2ee-history-sharing"
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appconfig
object PushConfig {
/**
* Note: pusher_app_id cannot exceed 64 chars.
*/
const val PUSHER_APP_ID: String = "im.vector.app.android"
}
@@ -35,11 +35,4 @@ object RageshakeConfig {
* The maximum number of log lines a rageshake can contain.
*/
const val MAX_LOG_LINES_SIZE = 1_000_000
/**
* The maximum size of the description. The bug report server copies it verbatim into a GitHub issue body,
* which GitHub rejects above 65536 characters. The remaining budget is left to the other reported fields,
* mainly the list of the omitted log file names.
*/
const val MAX_DESCRIPTION_SIZE = 40 * 1024
}
-10
View File
@@ -34,7 +34,6 @@ dependencies {
implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oauth.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api)
@@ -49,7 +48,6 @@ dependencies {
implementation(libs.coil)
implementation(projects.features.announcement.api)
implementation(projects.features.callnative.api)
implementation(projects.features.ftue.api)
implementation(projects.features.linknewdevice.api)
implementation(projects.features.share.api)
@@ -61,23 +59,15 @@ dependencies {
testCommonDependencies(libs)
testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test)
testImplementation(projects.libraries.accountselect.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.forward.test)
testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.signedout.test)
testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test)
@@ -8,15 +8,9 @@
package io.element.android.appnav
import com.bumble.appyx.core.navigation.NavKey
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.BackStackElement
import com.bumble.appyx.navmodel.backstack.BackStackElements
import com.bumble.appyx.navmodel.backstack.operation.BackStackOperation
import com.bumble.appyx.navmodel.backstack.operation.NewRoot
import com.bumble.appyx.navmodel.backstack.operation.Remove
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
/**
* Don't process NewRoot if the nav target already exists in the stack.
@@ -38,45 +32,3 @@ fun <T : Any> BackStack<T>.removeLast(element: T) {
} ?: return
accept(Remove(lastExpectedNavElement.key))
}
/**
* Replace the root element of the backstack, and keep the elements on top of it, contrary to [safeRoot] which removes all the elements.
*/
fun <T : Any> BackStack<T>.replaceRoot(element: T) {
accept(ReplaceRoot(element))
}
/**
* Replace the root element of the backstack, keeping the elements on top of it.
*
* Operation:
*
* [A, B, C] + ReplaceRoot(D) = [D, B, C]
* [A] + ReplaceRoot(D) = [D]
*/
@Parcelize
internal data class ReplaceRoot<T : Any>(
private val element: @RawValue T
) : BackStackOperation<T> {
override fun isApplicable(elements: BackStackElements<T>): Boolean =
elements.rootElement()?.key?.navTarget != element
override fun invoke(elements: BackStackElements<T>): BackStackElements<T> {
val root = elements.rootElement() ?: return elements
if (root.targetState == BackStack.State.ACTIVE) {
// Nothing on top of the root, so this is the same as NewRoot
return NewRoot(element).invoke(elements)
}
// The root is stashed below other elements: drop it, as Remove does for a non active element,
// and add the new root, stashed, in place of it.
val newRoot = BackStackElement(
key = NavKey(element),
fromState = BackStack.State.STASHED,
targetState = BackStack.State.STASHED,
operation = this,
)
return elements.map { if (it == root) newRoot else it }
}
private fun BackStackElements<T>.rootElement() = firstOrNull { it.targetState != BackStack.State.DESTROYED }
}
@@ -33,8 +33,7 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import kotlinx.parcelize.Parcelize
/**
@@ -60,7 +59,6 @@ class LoggedInAppScopeFlowNode(
interface Callback : Plugin {
fun navigateToBugReport()
fun navigateToAddAccount()
fun navigateToSession(sessionId: SessionId)
}
private val callback: Callback = callback()
@@ -93,10 +91,6 @@ class LoggedInAppScopeFlowNode(
override fun navigateToAddAccount() {
callback.navigateToAddAccount()
}
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
}
return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
}
@@ -8,51 +8,28 @@
package io.element.android.appnav
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import dev.zacsweers.metro.Inject
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@AssistedInject
@Inject
class LoggedInEventProcessor(
@Assisted private val snackbarDispatcher: SnackbarDispatcher,
private val snackbarDispatcher: SnackbarDispatcher,
private val roomMembershipObserver: RoomMembershipObserver,
private val sessionId: SessionId,
private val syncService: SyncService,
private val notificationCleaner: NotificationCleaner,
private val dispatchers: CoroutineDispatchers
) {
@AssistedFactory
interface Factory {
fun create(snackbarDispatcher: SnackbarDispatcher): LoggedInEventProcessor
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var currentChildScope: CoroutineScope? = null
private var observingJob: Job? = null
fun observeEvents(coroutineScope: CoroutineScope) {
if (currentChildScope != null) return
val childScope = coroutineScope.childScope(dispatchers.computation, "LoggedInEventProcessor")
.also { currentChildScope = it }
roomMembershipObserver.updates
observingJob = roomMembershipObserver.updates
.filter { !it.isUserInRoom }
.distinctUntilChanged()
.onEach { roomMemberShipUpdate ->
@@ -71,19 +48,12 @@ class LoggedInEventProcessor(
else -> Unit
}
}
.launchIn(childScope)
// Use the sync service heartbeat as a 'heartbeat' update to check existing notifications
syncService.heartBeat
.onEach {
notificationCleaner.clearReadRoomsNotifications(sessionId)
}
.launchIn(childScope)
.launchIn(coroutineScope)
}
fun stopObserving() {
currentChildScope?.cancel()
currentChildScope = null
observingJob?.cancel()
observingJob = null
}
private fun displayMessage(message: Int) {
@@ -11,7 +11,6 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
@@ -47,7 +46,6 @@ import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.callnative.api.NativeCallHost
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
@@ -56,8 +54,6 @@ import io.element.android.features.ftue.api.state.FtueService
import io.element.android.features.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.location.api.LocalMapTilerConfig
import io.element.android.features.location.api.LocationService
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
@@ -66,6 +62,8 @@ import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
@@ -85,16 +83,15 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.ui.common.nodes.EmptyState
import io.element.android.libraries.ui.common.nodes.emptyNode
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService
@@ -108,10 +105,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import java.time.Duration
import java.time.Instant
import java.util.Optional
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent
// The maximum number of room nodes that should be kept in the backstack at the same time.
@@ -135,9 +135,9 @@ class LoggedInFlowNode(
private val sessionCoroutineScope: CoroutineScope,
private val ftueService: FtueService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues,
private val nativeCallHost: NativeCallHost,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService,
@@ -153,8 +153,6 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val locationService: LocationService,
private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Placeholder,
@@ -170,26 +168,44 @@ class LoggedInFlowNode(
interface Callback : Plugin {
fun navigateToBugReport()
fun navigateToAddAccount()
fun navigateToSession(sessionId: SessionId)
}
private val callback: Callback = callback()
private val loggedInFlowProcessor = loggedInEventProcessorFactory.create(snackbarDispatcher)
private val loggedInFlowProcessor = LoggedInEventProcessor(
snackbarDispatcher = snackbarDispatcher,
roomMembershipObserver = matrixClient.roomMembershipObserver,
)
/**
* Display the incoming verification request, the app is expected to be in foreground.
*/
fun onIncomingVerificationRequest(verificationRequest: VerificationRequest.Incoming) {
lifecycleScope.launch {
// Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it
// Otherwise, the RoomList UI may be incorrectly displayed on top
withTimeout(5.seconds) {
backstack.elements.first { elements ->
elements.any { it.key.navTarget == NavTarget.Home }
private val verificationListener = object : SessionVerificationServiceListener {
override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) {
// Without this launch the rendering and actual state of this Appyx node's children gets out of sync, resulting in a crash.
// This might be because this method is called back from Rust in a background thread.
lifecycleScope.launch {
val receivedAt = Instant.now()
// Wait until the app is in foreground to display the incoming verification request
appNavigationStateService.appNavigationState.first { it.isInForeground }
// TODO there should also be a timeout for > 10 minutes elapsed since the request was created, but the SDK doesn't expose that info yet
val now = Instant.now()
val elapsedTimeSinceReceived = Duration.between(receivedAt, now).toKotlinDuration()
// Discard the incoming verification request if it has timed out
if (elapsedTimeSinceReceived > 2.minutes) {
Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.")
return@launch
}
}
backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest))
// Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it
// Otherwise, the RoomList UI may be incorrectly displayed on top
withTimeout(5.seconds) {
backstack.elements.first { elements ->
elements.any { it.key.navTarget == NavTarget.Home }
}
}
backstack.singleTop(NavTarget.IncomingVerificationRequest(verificationRequest))
}
}
}
@@ -204,6 +220,7 @@ class LoggedInFlowNode(
analyticsRoomListStateWatcher.start()
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration()
sessionCoroutineScope.launch {
// Wait for the network to be connected before pre-fetching the max file upload size
@@ -232,6 +249,7 @@ class LoggedInFlowNode(
onDestroy = {
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
matrixClient.sessionVerificationService.setListener(null)
analyticsRoomListStateWatcher.stop()
}
)
@@ -292,15 +310,16 @@ class LoggedInFlowNode(
@Parcelize
data object RoomDirectory : NavTarget
@Parcelize
data class IncomingShare(val shareIntentData: ShareIntentData) : NavTarget
@Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) {
// Rendered while the FtueState is Unknown. Render a loading state rather than nothing at all,
// since there is no guarantee that this state will be left.
NavTarget.Placeholder -> emptyNode(buildContext, state = EmptyState(delayBeforeShowingContent = 500.milliseconds))
NavTarget.Placeholder -> emptyNode(buildContext)
NavTarget.LoggedInPermanent -> {
val callback = object : LoggedInNode.Callback {
override fun navigateToNotificationTroubleshoot() {
@@ -311,11 +330,11 @@ class LoggedInFlowNode(
}
NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) {
override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
lifecycleScope.launch {
attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
clearBackstack = false,
)
}
@@ -355,14 +374,6 @@ class LoggedInFlowNode(
callback.navigateToBugReport()
}
override fun navigateToAddAccount() {
callback.navigateToAddAccount()
}
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
// HealthChat fork: open a user profile from the member directory tab.
override fun navigateToUserProfile(userId: UserId) {
backstack.push(NavTarget.UserProfile(userId))
@@ -459,10 +470,6 @@ class LoggedInFlowNode(
callback.navigateToAddAccount()
}
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
override fun navigateToLinkNewDevice() {
backstack.push(NavTarget.LinkNewDevice)
}
@@ -576,6 +583,30 @@ class LoggedInFlowNode(
},
)
}
is NavTarget.IncomingShare -> {
shareEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(shareIntentData = navTarget.shareIntentData),
callback = object : ShareEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) {
// Remove the incoming share screen
backstack.pop()
// Navigate to the room if the text/media was shared to a single one
roomIds.singleOrNull()?.let { roomId ->
lifecycleScope.launch {
// Wait until the incoming share screen is removed
backstack.elements.first { it.lastOrNull()?.key?.navTarget !is NavTarget.IncomingShare }
// Then attach the room
attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false)
}
}
}
},
)
}
is NavTarget.IncomingVerificationRequest -> {
incomingVerificationEntryPoint.createNode(
parentNode = this,
@@ -591,15 +622,6 @@ class LoggedInFlowNode(
}
}
/**
* Wait for the home screen to be attached, i.e. for the FTUE to be complete.
*/
internal suspend fun waitForHome() {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
}
suspend fun attachRoom(
roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String> = emptyList(),
@@ -644,14 +666,22 @@ class LoggedInFlowNode(
}
}
internal suspend fun attachIncomingShare(shareIntentData: ShareIntentData) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
attachChild<Node> {
backstack.push(
NavTarget.IncomingShare(shareIntentData)
)
}
}
@Composable
override fun View(modifier: Modifier) {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
}.collectAsState(SemanticColorsLightDark.default)
val mapTilerConfig by locationService.mapTilerConfig.collectAsState()
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
featureFlagService = featureFlagService,
@@ -659,22 +689,16 @@ class LoggedInFlowNode(
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
CompositionLocalProvider(LocalMapTilerConfig provides mapTilerConfig) {
val isOnline by syncService.isOnline.collectAsState()
// Outside the connectivity indicator so the two strips stack rather than fight: an
// offline banner during a call belongs under the call bar, not over it.
nativeCallHost.Render(modifier = modifier) { callContentModifier ->
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = callContentModifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
}
@@ -13,7 +13,6 @@ package io.element.android.appnav
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.lifecycle.subscribe
@@ -25,8 +24,6 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.features.announcement.api.Announcement
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView
@@ -36,11 +33,8 @@ import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class)
@@ -51,8 +45,6 @@ class NotLoggedInFlowNode(
private val loginEntryPoint: LoginEntryPoint,
private val imageLoaderHolder: ImageLoaderHolder,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
private val featureFlagService: FeatureFlagService,
private val announcementService: AnnouncementService,
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack(
initialElement = NavTarget.Root,
@@ -76,13 +68,6 @@ class NotLoggedInFlowNode(
override fun onBuilt() {
super.onBuilt()
analyticsColdStartWatcher.whenLoggingIn()
lifecycleScope.launch {
// Users who enter the login flow while the feature is available, either for the first time or to add
// another account, do not need to be informed about the multi-account feature.
if (featureFlagService.isFeatureEnabled(FeatureFlags.MultiAccount)) {
announcementService.onAnnouncementDismissed(Announcement.MultiAccount)
}
}
lifecycle.subscribe(
onResume = {
SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
@@ -32,6 +32,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.appnav.intent.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.room.RoomFlowNode
@@ -39,16 +40,10 @@ import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.root.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.appnav.verification.IncomingVerificationRequestData
import io.element.android.appnav.verification.IncomingVerificationRequestObserver
import io.element.android.appnav.verification.OtherSessionIncomingVerificationNode
import io.element.android.appnav.verification.OtherSessionVerificationNode
import io.element.android.features.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.share.api.ShareIntentData
import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
@@ -57,20 +52,17 @@ import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.appyx.rememberDelegateTransitionHandler
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached
import io.element.android.libraries.core.coroutine.withPreviousValue
import io.element.android.libraries.core.uri.ensureProtocol
import io.element.android.libraries.deeplink.api.DeeplinkData
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.accountprovider.AccountProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId
import io.element.android.libraries.matrix.api.core.asEventId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.oauth.api.OAuthAction
import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState
@@ -83,16 +75,11 @@ import io.element.android.services.appnavstate.api.ROOM_OPENED_FROM_NOTIFICATION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.parcelize.Parcelize
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ContributesNode(AppScope::class)
@AssistedInject
@@ -113,8 +100,6 @@ class RootFlowNode(
private val announcementService: AnnouncementService,
private val analyticsService: AnalyticsService,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
private val incomingVerificationRequestObserver: IncomingVerificationRequestObserver,
private val shareEntryPoint: ShareEntryPoint,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack(
@@ -124,13 +109,6 @@ class RootFlowNode(
buildContext = buildContext,
plugins = plugins
) {
/**
* Login params coming from a launch or new [Intent], waiting to be consumed by the not logged in flow.
* Kept here so that the root nav target can be computed from both the logged in state and the pending
* login params, whatever the order in which the intent and the first nav state emission are processed.
*/
private var pendingLoginParams: LoginParams? = null
override fun onBuilt() {
analyticsColdStartWatcher.start()
appCoroutineScope.launch {
@@ -142,8 +120,6 @@ class RootFlowNode(
observeNavState(false)
}
}
observeIncomingVerificationRequests()
observeRemovedSessions()
super.onBuilt()
}
@@ -176,7 +152,7 @@ class RootFlowNode(
}
}
LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(pendingLoginParams)
switchToNotLoggedInFlow(null)
}
}
}
@@ -218,93 +194,16 @@ class RootFlowNode(
}
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
pendingLoginParams = null
setRoot(NavTarget.LoggedInFlow(sessionId, navId))
restoreAllSessions()
}
/**
* Restore all the sessions with a valid token, so that they sync and can receive incoming verification requests.
*/
private fun restoreAllSessions() = lifecycleScope.launch {
sessionStore.getAllSessions()
.filter { it.isTokenValid }
.forEach { sessionData ->
matrixSessionCache.getOrRestore(SessionId(sessionData.userId)).onFailure {
Timber.e(it, "Failed to restore session ${sessionData.userId}")
}
}
}
/**
* Remove from the cache the sessions which have been removed from the session store, for instance after a logout.
*/
private fun observeRemovedSessions() {
sessionStore.sessionsFlow()
.map { sessions -> sessions.map { SessionId(it.userId) }.toSet() }
.withPreviousValue()
.onEach { (previous, current) ->
(previous.orEmpty() - current).forEach { sessionId ->
// The verification of a removed session cannot continue, for instance if the user has signed out from it
backstack.removeLast(NavTarget.OtherAccountSessionVerification(sessionId))
Timber.d("Session $sessionId removed, remove it from the cache")
matrixSessionCache.remove(sessionId)
}
}
.launchIn(lifecycleScope)
}
private fun observeIncomingVerificationRequests() {
incomingVerificationRequestObserver.incomingVerificationRequests()
.onEach(::onIncomingVerificationRequest)
.launchIn(lifecycleScope)
}
private fun onIncomingVerificationRequest(data: IncomingVerificationRequestData) = lifecycleScope.launch {
// Wait for a logged in flow to be displayed
val currentSessionId = withTimeoutOrNull(5.seconds) {
backstack.elements.map { elements ->
elements.firstNotNullOfOrNull { (it.key.navTarget as? NavTarget.LoggedInFlow)?.sessionId }
}.filterNotNull().first()
}
if (currentSessionId == null) {
Timber.w("Incoming verification request ${data.verificationRequest.details.flowId} discarded, no logged in flow.")
return@launch
}
if (currentSessionId == data.sessionId) {
waitForChildAttached<LoggedInAppScopeFlowNode, NavTarget> { navTarget ->
navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == currentSessionId
}
.attachSession()
.onIncomingVerificationRequest(data.verificationRequest)
} else {
// Display the request on top of the current session, without switching session
backstack.push(NavTarget.OtherAccountIncomingVerificationRequest(data.sessionId, data.verificationRequest))
}
backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
}
private fun switchToNotLoggedInFlow(params: LoginParams?) {
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
}
private fun switchToSignedOutFlow(sessionId: SessionId) {
pendingLoginParams = null
setRoot(NavTarget.SignedOutFlow(sessionId))
}
/**
* Set the root of the backstack. If an incoming share is displayed, keep it on top of the new root, so that the user can
* finish sharing, for instance with another account if the current one has been signed out.
*/
private fun setRoot(navTarget: NavTarget) {
val hasIncomingShare = backstack.elements.value.any { it.key.navTarget is NavTarget.IncomingShare }
if (hasIncomingShare) {
backstack.replaceRoot(navTarget)
} else {
backstack.safeRoot(navTarget)
}
backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
}
private suspend fun restoreSessionIfNeeded(
@@ -368,7 +267,8 @@ class RootFlowNode(
@Parcelize data class AccountSelect(
val currentSessionId: SessionId,
val permalinkData: PermalinkData,
val shareIntentData: ShareIntentData?,
val permalinkData: PermalinkData?,
) : NavTarget
@Parcelize data class NotLoggedInFlow(
@@ -384,20 +284,6 @@ class RootFlowNode(
) : NavTarget
@Parcelize data object BugReport : NavTarget
@Parcelize data class IncomingShare(
val sessionId: SessionId,
val shareIntentData: ShareIntentData,
) : NavTarget
@Parcelize data class OtherAccountIncomingVerificationRequest(
val sessionId: SessionId,
val verificationRequest: VerificationRequest.Incoming,
) : NavTarget
@Parcelize data class OtherAccountSessionVerification(
val sessionId: SessionId,
) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -416,12 +302,6 @@ class RootFlowNode(
override fun navigateToAddAccount() {
backstack.push(NavTarget.NotLoggedInFlow(null))
}
override fun navigateToSession(sessionId: SessionId) {
lifecycleScope.launch {
sessionStore.setLatestSession(sessionId.value)
}
}
}
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
val buildContext = if (savedNavState != null) {
@@ -439,7 +319,6 @@ class RootFlowNode(
}
override fun onDone() {
pendingLoginParams = null
backstack.pop()
}
}
@@ -458,73 +337,6 @@ class RootFlowNode(
)
}
NavTarget.SplashScreen -> emptyNode(buildContext)
is NavTarget.OtherAccountIncomingVerificationRequest -> {
if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) {
Timber.w("Couldn't find session ${navTarget.sessionId} for the incoming verification request")
lifecycleScope.launch { backstack.pop() }
return emptyNode(buildContext)
}
val inputs = OtherSessionIncomingVerificationNode.Inputs(
sessionId = navTarget.sessionId,
verificationRequest = navTarget.verificationRequest,
)
val callback = object : OtherSessionIncomingVerificationNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<OtherSessionIncomingVerificationNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.OtherAccountSessionVerification -> {
if (matrixSessionCache.getOrNull(navTarget.sessionId) == null) {
Timber.w("Couldn't find session ${navTarget.sessionId} to verify")
lifecycleScope.launch { backstack.pop() }
return emptyNode(buildContext)
}
val inputs = OtherSessionVerificationNode.Inputs(sessionId = navTarget.sessionId)
val callback = object : OtherSessionVerificationNode.Callback {
override fun onDone() {
backstack.pop()
}
}
createNode<OtherSessionVerificationNode>(buildContext, plugins = listOf(inputs, callback))
}
is NavTarget.IncomingShare -> {
val callback = object : ShareEntryPoint.Callback {
override fun onDone(sessionId: SessionId, roomIds: List<RoomId>) {
// Remove the incoming share screen
backstack.pop()
lifecycleScope.launch {
// Switch to the session which has been used to share the data, if necessary,
// and navigate to the room if the data was shared to a single one.
val loggedInFlowNode = attachSession(sessionId)
roomIds.singleOrNull()?.let { roomId ->
loggedInFlowNode.attachRoom(roomId.toRoomIdOrAlias(), clearBackstack = false)
}
}
}
override fun onCancel() {
backstack.pop()
}
override fun onSessionVerificationRequired(sessionId: SessionId) {
val navTarget = NavTarget.OtherAccountSessionVerification(sessionId)
if (backstack.elements.value.lastOrNull()?.key?.navTarget != navTarget) {
backstack.push(navTarget)
}
}
}
shareEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = ShareEntryPoint.Params(
sessionId = navTarget.sessionId,
shareIntentData = navTarget.shareIntentData,
),
callback = callback,
)
}
NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback {
override fun onDone() {
@@ -546,7 +358,13 @@ class RootFlowNode(
// Do not pop when the account is changed to avoid a UI flicker.
backstack.pop()
}
attachSession(sessionId).attachPermalinkData(navTarget.permalinkData)
attachSession(sessionId).apply {
if (navTarget.shareIntentData != null) {
attachIncomingShare(navTarget.shareIntentData)
} else if (navTarget.permalinkData != null) {
attachPermalinkData(navTarget.permalinkData)
}
}
}
}
@@ -581,8 +399,7 @@ class RootFlowNode(
}
private suspend fun onLoginLink(params: LoginParams) {
val accountProvider = AccountProvider.Generic(params.accountProvider)
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(accountProvider)) {
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
// Is there a session already?
val sessions = sessionStore.getAllSessions()
if (sessions.isNotEmpty()) {
@@ -601,7 +418,6 @@ class RootFlowNode(
Timber.w("Login link ignored, multi account is disabled")
}
} else {
pendingLoginParams = params
switchToNotLoggedInFlow(params)
}
} else {
@@ -616,12 +432,21 @@ class RootFlowNode(
// No session, open login
switchToNotLoggedInFlow(null)
} else {
// Wait for the current session to be restored and for the FTUE to be complete, so that the session is verified,
// then display the incoming share on top of it.
// The incoming share does not belong to the current session: in case of multiple accounts, the user can switch account
// from the room select screen, and the share is kept if the current session is signed out in the meantime.
attachSession(latestSessionId).waitForHome()
backstack.push(NavTarget.IncomingShare(latestSessionId, shareIntentData))
// wait for the current session to be restored
val loggedInFlowNode = attachSession(latestSessionId)
if (sessionStore.numberOfSessions() > 1) {
// Several accounts, let the user choose which one to use
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
shareIntentData = shareIntentData,
permalinkData = null,
)
)
} else {
// Only one account, directly attach the incoming share node.
loggedInFlowNode.attachIncomingShare(shareIntentData)
}
}
}
@@ -644,6 +469,7 @@ class RootFlowNode(
backstack.push(
NavTarget.AccountSelect(
currentSessionId = latestSessionId,
shareIntentData = null,
permalinkData = permalinkData,
)
)
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.session
package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap
@@ -21,9 +21,6 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.AnalyticsUserData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
@@ -44,12 +41,6 @@ class MatrixSessionCache(
private val analyticsService: AnalyticsService,
) : MatrixClientProvider {
private val sessionIdsToMatrixSession = ConcurrentHashMap<SessionId, InMemoryMatrixSession>()
private val _matrixClients = MutableStateFlow<List<MatrixClient>>(emptyList())
/**
* The [MatrixClient]s currently in the cache.
*/
val matrixClients: StateFlow<List<MatrixClient>> = _matrixClients.asStateFlow()
private val restoreMutex = Mutex()
init {
@@ -60,12 +51,10 @@ class MatrixSessionCache(
fun removeAll() {
sessionIdsToMatrixSession.clear()
updateMatrixClients()
}
fun remove(sessionId: SessionId) {
sessionIdsToMatrixSession.remove(sessionId)
updateMatrixClients()
}
override fun getOrNull(sessionId: SessionId): MatrixClient? {
@@ -126,20 +115,15 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create(
matrixClient = matrixClient,
syncService = matrixClient.syncService,
sessionCoroutineScope = matrixClient.sessionCoroutineScope,
)
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
matrixClient = matrixClient,
syncOrchestrator = syncOrchestrator,
)
updateMatrixClients()
syncOrchestrator.start()
}
private fun updateMatrixClients() {
_matrixClients.value = sessionIdsToMatrixSession.values.map { it.matrixClient }
}
}
private data class InMemoryMatrixSession(
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.session
package io.element.android.appnav.di
import androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted
@@ -16,7 +16,7 @@ import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction
@@ -28,20 +28,16 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@AssistedInject
class SyncOrchestrator(
@Assisted private val matrixClient: MatrixClient,
@Assisted private val syncService: SyncService,
@Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor,
@@ -51,25 +47,17 @@ class SyncOrchestrator(
@AssistedFactory
interface Factory {
fun create(
matrixClient: MatrixClient,
syncService: SyncService,
sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator
}
private val tag = "SyncOrchestrator"
private val syncService = matrixClient.syncService
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false)
/**
* Number of consecutive attempts at restarting a sync service which stopped on its own.
* Reset once the sync service has been running for [RESTART_BACKOFF_RESET_DELAY].
*/
private val restartAttempt = AtomicInteger(0)
/**
* Starting observing the app state and network state to start/stop the sync service.
*
@@ -101,23 +89,12 @@ class SyncOrchestrator(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun observeStates() = coroutineScope.launch {
Timber.tag(tag).d("start observing the app and network state")
// Reset the restart backoff once the sync service has been steadily running for a while. Note that
// SyncService.startSync() moves the state to Running right away, so resetting on any Running value
// would defeat the backoff when the sync service dies again immediately after each restart.
syncService.syncState
.debounce(RESTART_BACKOFF_RESET_DELAY)
.onEach { syncState ->
if (syncState == SyncState.Running) {
restartAttempt.set(0)
}
}
.launchIn(this)
val isAppActiveFlows = listOf(
appForegroundStateService.isInForeground,
appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall,
appForegroundStateService.isSharingLiveLocation,
appForegroundStateService.isSharingLiveLocation
)
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine(
@@ -126,11 +103,6 @@ class SyncOrchestrator(
networkMonitor.connectivity,
isAppActiveFlow,
) { syncState, networkState, isAppActive ->
if (matrixClient.isShuttingDown) {
Timber.tag(tag).d("Matrix client is shutting down, no need to start the sync service again")
return@combine SyncStateAction.NoOp
}
val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
@@ -138,26 +110,14 @@ class SyncOrchestrator(
SyncStateAction.StopSync
} else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) {
SyncStateAction.StartSync
} else if (syncState == SyncState.Error && isAppActive && isNetworkAvailable) {
// The sync service has given up, for instance because the homeserver has expired our sliding sync
// session and answered 400 M_UNKNOWN_POS. Nothing else will ever restart it, so do it from here,
// else the application is stuck until its data is cleared.
SyncStateAction.RestartSync
} else {
SyncStateAction.NoOp
}
}
.distinctUntilChanged()
.debounce { action ->
when (action) {
// Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
SyncStateAction.StopSync -> 3.seconds
// Back off before restarting a dead sync service, to avoid hammering a failing homeserver.
// A state change in the meantime cancels the pending restart.
SyncStateAction.RestartSync -> restartDelay(restartAttempt.get())
SyncStateAction.StartSync,
SyncStateAction.NoOp -> 0.seconds
}
// Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
if (action == SyncStateAction.StopSync) 3.seconds else 0.seconds
}
.onCompletion {
Timber.tag(tag).d("has been stopped")
@@ -167,11 +127,6 @@ class SyncOrchestrator(
SyncStateAction.StartSync -> {
syncService.startSync()
}
SyncStateAction.RestartSync -> {
val attempt = restartAttempt.incrementAndGet()
Timber.tag(tag).w("sync service is dead, restarting it (attempt $attempt)")
syncService.startSync()
}
SyncStateAction.StopSync -> {
syncService.stopSync()
}
@@ -181,35 +136,8 @@ class SyncOrchestrator(
}
}
@VisibleForTesting
internal val FIRST_RESTART_DELAY = 1.seconds
/**
* Exponential backoff, from [FIRST_RESTART_DELAY], doubling on every attempt, up to [FIRST_RESTART_DELAY * 32].
*/
@VisibleForTesting
internal val RESTART_BACKOFF_DELAYS = listOf(
FIRST_RESTART_DELAY,
FIRST_RESTART_DELAY * 2,
FIRST_RESTART_DELAY * 4,
FIRST_RESTART_DELAY * 8,
FIRST_RESTART_DELAY * 16,
FIRST_RESTART_DELAY * 32,
)
/**
* Duration during which the sync service has to stay running before the restart backoff is reset.
*/
@VisibleForTesting
internal val RESTART_BACKOFF_RESET_DELAY = 30.seconds
private fun restartDelay(attempt: Int): Duration {
return RESTART_BACKOFF_DELAYS[attempt.coerceIn(0, RESTART_BACKOFF_DELAYS.lastIndex)]
}
private enum class SyncStateAction {
StartSync,
RestartSync,
StopSync,
NoOp,
}
@@ -76,7 +76,6 @@ class IntentResolver(
}
private fun Intent.canBeIgnored(): Boolean {
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true
}
@@ -8,11 +8,8 @@
package io.element.android.appnav.loggedin
sealed interface LoggedInEvent {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvent
data object CheckSlidingSyncProxyAvailability : LoggedInEvent
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvent
data object DismissLocalNetworkPermissionPrompt : LoggedInEvent
data object RequestLocationNetworkPermission : LoggedInEvent
sealed interface LoggedInEvents {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
data object CheckSlidingSyncProxyAvailability : LoggedInEvents
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
}
@@ -8,7 +8,6 @@
package io.element.android.appnav.loggedin
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
@@ -16,7 +15,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
@@ -38,10 +36,6 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService
@@ -64,12 +58,7 @@ class LoggedInPresenter(
private val encryptionService: EncryptionService,
private val buildMeta: BuildMeta,
private val networkMonitor: NetworkMonitor,
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<LoggedInState> {
private val localNetworkPermissionsPresenter: PermissionsPresenter =
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
@Composable
override fun present(): LoggedInState {
val coroutineScope = rememberCoroutineScope()
@@ -105,18 +94,9 @@ class LoggedInPresenter(
}
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
val isOnline by syncService.isOnline.collectAsState()
val hasNetwork by networkMonitor.connectivity.collectAsState()
val syncIndicatorState by remember {
val showSyncSpinner by remember {
derivedStateOf {
when {
isOnline -> if (syncIndicator == RoomListService.SyncIndicator.Show) {
SyncIndicatorState.Syncing
} else {
SyncIndicatorState.Hidden
}
hasNetwork == NetworkStatus.Connected -> SyncIndicatorState.ServerUnreachable
else -> SyncIndicatorState.Hidden
}
isOnline && syncIndicator == RoomListService.SyncIndicator.Show
}
}
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
@@ -129,12 +109,6 @@ class LoggedInPresenter(
}.launchIn(this)
}
LaunchedEffect(Unit) {
// Keep automatic call status (m.call) in sync with homeserver support.
val enabled = matrixClient.isUserStatusSupported().getOrDefault(false)
matrixClient.enableAutomaticCallStatus(enabled)
}
val networkConnectivity by networkMonitor.connectivity.collectAsState()
LaunchedEffect(networkConnectivity) {
if (networkConnectivity == NetworkStatus.Connected) {
@@ -143,24 +117,9 @@ class LoggedInPresenter(
}
}
val localNetworkPermissionState = localNetworkPermissionsPresenter.present()
var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) }
val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) {
value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl)
}
LaunchedEffect(localNetworkPermissionState.permissionGranted) {
if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false
}
val localNetworkPermissionDialog = when {
!advisorRequestsPrompt -> LocalNetworkPermissionDialog.None
localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None
localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale
else -> LocalNetworkPermissionDialog.Settings
}
fun handleEvent(event: LoggedInEvent) {
fun handleEvent(event: LoggedInEvents) {
when (event) {
is LoggedInEvent.CloseErrorDialog -> {
is LoggedInEvents.CloseErrorDialog -> {
pusherRegistrationState.value = AsyncData.Uninitialized
if (event.doNotShowAgain) {
coroutineScope.launch {
@@ -168,34 +127,22 @@ class LoggedInPresenter(
}
}
}
LoggedInEvent.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false)
}
LoggedInEvent.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
LoggedInEvents.LogoutAndMigrateToNativeSlidingSync -> coroutineScope.launch {
// Force the logout since Native Sliding Sync is already enforced by the SDK
matrixClient.logout(userInitiated = true, ignoreSdkError = true)
}
LoggedInEvent.DismissLocalNetworkPermissionPrompt -> {
localNetworkPromptDismissedThisSession = true
}
LoggedInEvent.RequestLocationNetworkPermission -> {
if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) {
localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
} else {
localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions)
}
localNetworkPromptDismissedThisSession = true
}
}
}
return LoggedInState(
syncIndicatorState = syncIndicatorState,
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = buildMeta.applicationName,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = ::handleEvent,
)
}
@@ -9,20 +9,12 @@
package io.element.android.appnav.loggedin
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
enum class SyncIndicatorState {
Hidden,
Syncing,
ServerUnreachable,
}
data class LoggedInState(
val syncIndicatorState: SyncIndicatorState,
val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean,
val appName: String,
val localNetworkPermissionDialog: LocalNetworkPermissionDialog,
val eventSink: (LoggedInEvent) -> Unit,
val eventSink: (LoggedInEvents) -> Unit,
)
@@ -10,34 +10,28 @@ package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> {
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState>
get() = sequenceOf(
aLoggedInState(),
aLoggedInState(syncIndicatorState = SyncIndicatorState.Syncing),
aLoggedInState(syncIndicatorState = SyncIndicatorState.ServerUnreachable),
aLoggedInState(showSyncSpinner = true),
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
aLoggedInState(forceNativeSlidingSyncMigration = true),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings),
)
}
fun aLoggedInState(
syncIndicatorState: SyncIndicatorState = SyncIndicatorState.Hidden,
showSyncSpinner: Boolean = false,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
forceNativeSlidingSyncMigration: Boolean = false,
appName: String = "Element X",
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
) = LoggedInState(
syncIndicatorState = syncIndicatorState,
showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = appName,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = {},
)
@@ -25,7 +25,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import io.element.android.libraries.matrix.api.exception.isNetworkError
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView
import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.libraries.ui.strings.CommonStrings
@@ -37,7 +36,7 @@ fun LoggedInView(
) {
OnLifecycleEvent { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
state.eventSink(LoggedInEvent.CheckSlidingSyncProxyAvailability)
state.eventSink(LoggedInEvents.CheckSlidingSyncProxyAvailability)
}
}
Box(
@@ -47,7 +46,7 @@ fun LoggedInView(
) {
SyncStateView(
modifier = Modifier.align(Alignment.TopCenter),
state = state.syncIndicatorState,
isVisible = state.showSyncSpinner,
)
}
when (state.pusherRegistrationState) {
@@ -63,10 +62,10 @@ fun LoggedInView(
content = stringResource(id = CommonStrings.common_error_registering_pusher_android, reason),
cancelText = stringResource(id = CommonStrings.common_settings),
onDismiss = {
state.eventSink(LoggedInEvent.CloseErrorDialog(it))
state.eventSink(LoggedInEvents.CloseErrorDialog(it))
},
onCancel = {
state.eventSink(LoggedInEvent.CloseErrorDialog(false))
state.eventSink(LoggedInEvents.CloseErrorDialog(false))
navigateToNotificationTroubleshoot()
}
)
@@ -79,16 +78,10 @@ fun LoggedInView(
ForceNativeSlidingSyncMigrationDialog(
appName = state.appName,
onSubmit = {
state.eventSink(LoggedInEvent.LogoutAndMigrateToNativeSlidingSync)
state.eventSink(LoggedInEvents.LogoutAndMigrateToNativeSlidingSync)
}
)
}
LocalNetworkPermissionDialogView(
dialog = state.localNetworkPermissionDialog,
onSubmit = { state.eventSink(LoggedInEvent.RequestLocationNetworkPermission) },
onDismiss = { state.eventSink(LoggedInEvent.DismissLocalNetworkPermissionPrompt) },
)
}
private fun Throwable.getReason(): String? {
@@ -124,7 +117,7 @@ private fun ForceNativeSlidingSyncMigrationDialog(
@PreviewsDayNight
@Composable
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStatePreviewParam::class) state: LoggedInState) = ElementPreview {
internal fun LoggedInViewPreview(@PreviewParameter(LoggedInStateProvider::class) state: LoggedInState) = ElementPreview {
LoggedInView(
state = state,
navigateToNotificationTroubleshoot = {},
@@ -41,7 +41,7 @@ class SendQueues(
fun launchIn(coroutineScope: CoroutineScope) {
combine(
syncService.syncState,
matrixClient.sendQueueDisabledFlow().onEach { Timber.tag("SendQueues").d("Send queue disabled for room: $it") },
matrixClient.sendQueueDisabledFlow(),
) { syncState, _ -> syncState }
.debounce(SEND_QUEUES_RETRY_DELAY_MILLIS)
.onEach { syncState ->
@@ -25,23 +25,18 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun SyncStateView(
state: SyncIndicatorState,
isVisible: Boolean,
modifier: Modifier = Modifier
) {
AnimatedVisibility(
visible = state != SyncIndicatorState.Hidden,
visible = isVisible,
modifier = modifier,
enter = fadeIn(spring(stiffness = 500F)),
exit = fadeOut(spring(stiffness = 500F)),
) {
when (state) {
SyncIndicatorState.ServerUnreachable -> AsyncIndicator.Failure(
text = stringResource(id = CommonStrings.common_server_unreachable),
)
else -> AsyncIndicator.Loading(
text = stringResource(id = CommonStrings.common_syncing),
)
}
AsyncIndicator.Loading(
text = stringResource(id = CommonStrings.common_syncing),
)
}
}
@@ -51,18 +46,7 @@ internal fun SyncStateViewPreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
SyncStateView(
state = SyncIndicatorState.Syncing
)
}
}
@PreviewsDayNight
@Composable
internal fun SyncStateViewServerUnreachablePreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
SyncStateView(
state = SyncIndicatorState.ServerUnreachable
isVisible = true
)
}
}
@@ -28,8 +28,8 @@ import io.element.android.appnav.room.joined.JoinedRoomFlowNode
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.appnav.room.joined.LoadingRoomNodeView
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
import io.element.android.features.roomaliasresolver.api.RoomAliasResolverEntryPoint
import io.element.android.features.roomaliasresolver.api.RoomAliasResolverEntryPoint.Params
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint.Params
import io.element.android.features.roomdirectory.api.RoomDescription
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
@@ -6,9 +6,12 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.appnav.room.joined
import android.os.Parcelable
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -201,7 +201,7 @@ class JoinedRoomLoadedFlowNode(
} else {
(graph as TimelineBindings).timelineProvider
}
val params = ForwardEntryPoint.Params(listOf(navTarget.eventId), timelineProvider)
val params = ForwardEntryPoint.Params(navTarget.eventId, timelineProvider)
val callback = object : ForwardEntryPoint.Callback {
override fun onDone(roomIds: List<RoomId>) {
backstack.pop()
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -28,7 +29,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.utils.DelayedVisibility
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
import io.element.android.libraries.matrix.ui.room.LoadingRoomStatePreviewParam
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
@@ -67,6 +68,7 @@ fun LoadingRoomNodeView(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LoadingRoomTopBar(
onBackClick: () -> Unit,
@@ -82,7 +84,7 @@ private fun LoadingRoomTopBar(
@PreviewsDayNight
@Composable
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStatePreviewParam::class) state: LoadingRoomState) = ElementPreview {
internal fun LoadingRoomNodeViewPreview(@PreviewParameter(LoadingRoomStateProvider::class) state: LoadingRoomState) = ElementPreview {
LoadingRoomNodeView(
state = state,
onBackClick = {}
@@ -11,9 +11,9 @@ package io.element.android.appnav.root
import com.bumble.appyx.core.state.MutableSavedStateMap
import com.bumble.appyx.core.state.SavedStateMap
import dev.zacsweers.metro.Inject
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.appnav.di.MatrixSessionCache
import io.element.android.features.preferences.api.CacheService
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.Flow
@@ -14,7 +14,7 @@ import io.element.android.features.rageshake.api.detection.aRageshakeDetectionSt
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.aAppErrorState
open class RootStatePreviewParam : PreviewParameterProvider<RootState> {
open class RootStateProvider : PreviewParameterProvider<RootState> {
override val values: Sequence<RootState>
get() = sequenceOf(
aRootState().copy(
@@ -60,7 +60,7 @@ fun RootView(
@PreviewsDayNight
@Composable
internal fun RootViewPreview(@PreviewParameter(RootStatePreviewParam::class) rootState: RootState) = ElementPreview {
internal fun RootViewPreview(@PreviewParameter(RootStateProvider::class) rootState: RootState) = ElementPreview {
RootView(
state = rootState,
onOpenBugReport = {},
@@ -1,81 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.verification
import dev.zacsweers.metro.Inject
import io.element.android.appnav.session.MatrixSessionCache
import io.element.android.libraries.core.coroutine.withPreviousValue
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.services.appnavstate.api.AppForegroundStateService
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import timber.log.Timber
import kotlin.time.Duration.Companion.minutes
data class IncomingVerificationRequestData(
val sessionId: SessionId,
val verificationRequest: VerificationRequest.Incoming,
)
/**
* Listens to the incoming verification requests of all the sessions in [MatrixSessionCache], not only the current one.
*/
@Inject
class IncomingVerificationRequestObserver(
private val matrixSessionCache: MatrixSessionCache,
private val appForegroundStateService: AppForegroundStateService,
) {
/**
* Emits the incoming verification requests once the app is in foreground.
* Requests received while the app is in background for too long are discarded.
*/
fun incomingVerificationRequests(): Flow<IncomingVerificationRequestData> = channelFlow {
launch {
matrixSessionCache.matrixClients.withPreviousValue().collect { (previousMatrixClients, matrixClients) ->
val previousMatrixClientSet = previousMatrixClients.orEmpty().toSet()
// Remove the listener of the clients which are not in the cache anymore
(previousMatrixClientSet - matrixClients.toSet()).forEach { matrixClient ->
matrixClient.sessionVerificationService.setListener(null)
}
(matrixClients - previousMatrixClientSet).forEach { matrixClient ->
val sessionId = matrixClient.sessionId
matrixClient.sessionVerificationService.setListener(
object : SessionVerificationServiceListener {
override fun onIncomingSessionRequest(verificationRequest: VerificationRequest.Incoming) {
launch {
// Wait until the app is in foreground to display the incoming verification request
// TODO there should also be a timeout for > 10 minutes elapsed since the request was created,
// but the SDK doesn't expose that info yet
val isInForeground = withTimeoutOrNull(2.minutes) {
appForegroundStateService.isInForeground.first { it }
}
if (isInForeground == null) {
Timber.w("Incoming verification request ${verificationRequest.details.flowId} discarded due to timeout.")
} else {
send(IncomingVerificationRequestData(sessionId, verificationRequest))
}
}
}
}
)
}
}
}
awaitClose {
matrixSessionCache.matrixClients.value.forEach { matrixClient ->
matrixClient.sessionVerificationService.setListener(null)
}
}
}
}
@@ -1,92 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.verification
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.SessionGraphFactory
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import kotlinx.parcelize.Parcelize
/**
* Display an incoming verification request received by a session which is not the current one, without switching the current session.
* It sets up the Session graph of the session receiving the request.
*/
@ContributesNode(AppScope::class)
@AssistedInject
class OtherSessionIncomingVerificationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
sessionGraphFactory: SessionGraphFactory,
matrixClientProvider: MatrixClientProvider,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
) : ParentNode<OtherSessionIncomingVerificationNode.NavTarget>(
navModel = PermanentNavModel(
navTargets = setOf(NavTarget),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
), DependencyInjectionGraphOwner {
interface Callback : Plugin {
fun onDone()
}
@Parcelize
object NavTarget : Parcelable
data class Inputs(
val sessionId: SessionId,
val verificationRequest: VerificationRequest.Incoming,
) : NodeInputs
private val callback: Callback = callback()
private val inputs: Inputs = inputs()
override val graph = sessionGraphFactory.create(
requireNotNull(matrixClientProvider.getOrNull(inputs.sessionId)) { "No MatrixClient for session ${inputs.sessionId}" }
)
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return incomingVerificationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
params = IncomingVerificationEntryPoint.Params(inputs.verificationRequest),
callback = object : IncomingVerificationEntryPoint.Callback {
override fun onDone() {
callback.onDone()
}
},
)
}
@Composable
override fun View(modifier: Modifier) {
Children(
navModel = navModel,
modifier = modifier,
)
}
}
@@ -1,89 +0,0 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.appnav.verification
import android.os.Parcelable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.composable.Children
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.node.ParentNode
import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.appnav.di.SessionGraphFactory
import io.element.android.features.ftue.api.SessionVerificationEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.callback
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.parcelize.Parcelize
/**
* Display the verification of a session which is not the current one, without switching the current session.
* It sets up the Session graph of the session to verify.
*/
@ContributesNode(AppScope::class)
@AssistedInject
class OtherSessionVerificationNode(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
sessionGraphFactory: SessionGraphFactory,
matrixClientProvider: MatrixClientProvider,
private val sessionVerificationEntryPoint: SessionVerificationEntryPoint,
) : ParentNode<OtherSessionVerificationNode.NavTarget>(
navModel = PermanentNavModel(
navTargets = setOf(NavTarget),
savedStateMap = buildContext.savedStateMap,
),
buildContext = buildContext,
plugins = plugins
), DependencyInjectionGraphOwner {
interface Callback : Plugin {
fun onDone()
}
@Parcelize
object NavTarget : Parcelable
data class Inputs(
val sessionId: SessionId,
) : NodeInputs
private val callback: Callback = callback()
private val inputs: Inputs = inputs()
override val graph = sessionGraphFactory.create(
requireNotNull(matrixClientProvider.getOrNull(inputs.sessionId)) { "No MatrixClient for session ${inputs.sessionId}" }
)
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return sessionVerificationEntryPoint.createNode(
parentNode = this,
buildContext = buildContext,
callback = object : SessionVerificationEntryPoint.Callback {
override fun onDone() {
callback.onDone()
}
},
)
}
@Composable
override fun View(modifier: Modifier) {
Children(
navModel = navModel,
modifier = modifier,
)
}
}
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"تسجيل الخروج والترقية"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"لم يعد %1$s يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"لم يعد خادمك المنزلي يدعم البروتوكول القديم. يرجى تسجيل الخروج ثم تسجيل الدخول مرة أخرى لمتابعة استخدام التطبيق."</string>
</resources>
@@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Odhlásit se a upgradovat"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s již nepodporuje starý protokol. Odhlaste se a znovu přihlaste, abyste mohli pokračovat v používání aplikace."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Váš poskytovatel účtu již nepodporuje starý protokol. Chcete-li aplikaci nadále používat, odhlaste se a znovu se přihlaste."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Váš domovský server již nepodporuje starý protokol. Chcete-li pokračovat v používání aplikace, odhlaste se a znovu se přihlaste."</string>
</resources>
@@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Abmelden und aktualisieren"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"%1$s unterstützt das alte Protokoll nicht mehr. Bitte melde dich ab und wieder an, um die App weiter nutzen zu können."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Dein Kontoanbieter unterstützt das alte Protokoll nicht mehr. Bitte melde dich ab und wieder an, um die App weiter zu nutzen."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Dein Homeserver unterstützt das alte Protokoll nicht mehr. Bitte logge dich aus und melde dich wieder an, um die App weiter zu nutzen."</string>
</resources>
@@ -2,5 +2,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="banner_migrate_to_native_sliding_sync_action">"Αποσύνδεση &amp; Αναβάθμιση"</string>
<string name="banner_migrate_to_native_sliding_sync_app_force_logout_title">"Το %1$s δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδεθείτε και συνδεθείτε ξανά για να συνεχίσετε να χρησιμοποιείτε την εφαρμογή."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο πάροχος του λογαριασμού σας δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδεθείτε και συνδεθείτε ξανά για να συνεχίσετε να χρησιμοποιείτε την εφαρμογή."</string>
<string name="banner_migrate_to_native_sliding_sync_force_logout_title">"Ο οικιακός διακομιστής σου δεν υποστηρίζει πλέον το παλιό πρωτόκολλο. Αποσυνδέσου και συνδέσου ξανά για να συνεχίσεις να χρησιμοποιείς την εφαρμογή."</string>
</resources>

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