5 Commits

Author SHA1 Message Date
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
5980 changed files with 26360 additions and 86839 deletions
+8 -37
View File
@@ -159,31 +159,6 @@ fixing the changelog over relying on the fallback.
- [ ] Internal testers get the update automatically if they're enrolled. - [ ] Internal testers get the update automatically if they're enrolled.
- [ ] AAB + `mapping.txt` are attached to the workflow run, retained 90 days. - [ ] AAB + `mapping.txt` are attached to the workflow run, retained 90 days.
**A green "Build Release AAB" step does not mean the release shipped.** The
upload step logs `Successfully uploaded 1 artifacts` before it commits the Play
edit; if the commit fails, the AAB is uploaded but no release exists on the
track. Always check the `Upload to Play Store` step, not just the job's build
steps.
## Troubleshooting: `changesNotSentForReview must not be set`
```
Successfully uploaded 1 artifacts
Committing the Edit
##[error]Changes are sent for review automatically.
The query parameter changesNotSentForReview must not be set.
```
Google flips this per app: once Play sends an app's changes for review
automatically, the Edits API rejects `changesNotSentForReview` instead of
ignoring it. The 26.07.0 release (2026-07-27) committed fine with the flag
set; 26.08.0 (2026-08-03) failed with it. The parameter is no longer passed —
if it ever needs to come back, gate it rather than setting it unconditionally.
Recovery: the failed edit is never committed, so the `versionCode` is not
consumed. Re-run via `workflow_dispatch` with the same version — no tag move
and no version bump needed.
## Required GitHub Actions secrets ## Required GitHub Actions secrets
The pipeline depends on these repository secrets. Missing any causes the The pipeline depends on these repository secrets. Missing any causes the
@@ -261,12 +236,12 @@ compliance metadata review.
- **Production rollout gone wrong** → Play Console → Production → **Halt - **Production rollout gone wrong** → Play Console → Production → **Halt
rollout** (works during staged rollout), then ship a hotfix as above. 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 When merging a newer upstream Element X release tag into `release/healthchat`
tag, never by merging — the procedure, Git LFS handling and force-push live in (temporarily add `https://github.com/element-hq/element-x-android` as a
the `syncing-upstream-healthchat` skill. The HealthChat-only facts that must remote, fetch, merge, remove the remote — never keep a permanent upstream
survive every sync: remote):
- **`.github/workflows/healthchat-release-playstore.yml`**, - **`.github/workflows/healthchat-release-playstore.yml`**,
**`HEALTHCHAT_CHANGES.md`**, and **`HEALTHCHAT_CHANGES.md`**, and
@@ -279,13 +254,9 @@ survive every sync:
Upstream uses `io.element.android.x` / `Element X` / nulls. Upstream uses `io.element.android.x` / `Element X` / nulls.
- **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` / - **`appconfig/.../ApplicationConfig.kt`** — `PRODUCTION_APPLICATION_NAME` /
`DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`. `DESKTOP_APPLICATION_NAME = "HealthChat"` must survive. Upstream: `Element`.
- **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** — the - **`features/enterprise/impl-foss/.../DefaultEnterpriseService.kt`** —
homeserver lock: `accountProviderAllowList() = listOf(Generic("matrix.healthchat.ch"))`, `defaultHomeserverList() = listOf("matrix.healthchat.ch")` must survive.
`canConnectToAnyAccountProvider() = false`, `isAllowedToConnectToAccountProvider()` Upstream returns an empty list.
= 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.
- **`app/build.gradle.kts`** — the `healthchat` signing config and the - **`app/build.gradle.kts`** — the `healthchat` signing config and the
`appicon/healthchat` dependency must survive; upstream signs release with `appicon/healthchat` dependency must survive; upstream signs release with
the debug keystore and depends on `appicon/element`. the debug keystore and depends on `appicon/element`.
@@ -1,215 +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`.
## Embedded Element Call: test every bump against the iPhone
`gradle/libs.versions.toml` pins `element_call_embedded`, the web app the call screen loads. Upstream
lets Renovate bump it; each bump ships a fresh matrix-js-sdk snapshot, and the Rust SDK on both
HealthChat apps parses the call membership that js-sdk writes. 26.10.0 shipped Element Call 0.28.0,
whose js-sdk dropped `livekit_alias` from `foci_preferred`; ruma requires the field, so the Rust SDK
discarded the membership and the iPhone stopped ringing for Android calls (Android's own
ring-cancel detection broke the same way). Pinned to 0.26.1 in 26.10.1, the version iOS embeds; the
hosted Element Call on the server (`matrix_element_call_version` in the playbook inventory) must stay
on the same version or web and desktop calls stop ringing. 0.27.0 is on Maven without a release. Before lifting the pin or
taking a newer bump: start a call from Android, the current iPhone build must ring, and the room
state must still parse (`has_active_room_call`). Lift only when both apps run a Rust SDK that
accepts memberships without the alias.
## 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: | 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. 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: options:
- 'No'
- 'Yes' - 'Yes'
- 'No'
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
id: pr id: pr
attributes: attributes:
label: Will you submit a Pull Request? label: Are you willing to provide a PR?
description: | 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: options:
- 'No'
- 'Yes' - 'Yes'
- 'No'
validations: validations:
required: true required: true
+2 -2
View File
@@ -1,11 +1,11 @@
name: Enhancement request 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] labels: [T-Enhancement]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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 - type: textarea
id: usecase id: usecase
attributes: attributes:
-4
View File
@@ -28,10 +28,6 @@ changelog:
labels: labels:
- PR-Dependencies - PR-Dependencies
- title: 💼 Element Pro
labels:
- PR-Element-Pro
- title: Others - title: Others
labels: labels:
- PR-misc - PR-misc
+1 -22
View File
@@ -7,17 +7,10 @@
"PR-Dependencies", "PR-Dependencies",
], ],
"ignoreDeps": [ "ignoreDeps": [
"string:app_name",
"gradle", "gradle",
], ],
"minimumReleaseAge": "7 days", "minimumReleaseAge": "7 days",
"hostRules": [
{
"matchHost": "https://repo1.maven.org/maven2/",
"concurrentRequestLimit": 5,
"maxRequestsPerSecond": 5,
"timeout": 10000,
}
],
"packageRules": [ "packageRules": [
{ {
"matchPackageNames": [ "matchPackageNames": [
@@ -25,20 +18,6 @@
"/^io.element.android/", "/^io.element.android/",
], ],
"minimumReleaseAge": null, "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", "groupName": "kotlin",
+8 -36
View File
@@ -7,32 +7,18 @@ on:
push: push:
branches: [ develop ] branches: [ develop ]
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
build: build:
name: Build APKs name: Build APKs
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
permissions: permissions:
# A job-level block replaces the workflow-level one, so contents:read has to be
# repeated here or checkout fails on this private fork.
contents: read
# For NejcZdovc/comment-pr # For NejcZdovc/comment-pr
pull-requests: write pull-requests: write
strategy: strategy:
@@ -45,9 +31,6 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -61,19 +44,19 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APKs - name: Assemble debug APKs
@@ -91,7 +74,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:assembleGplayDebug app:assembleFDroidDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug APKs - name: Upload debug APKs
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: elementx-debug name: elementx-debug
path: | path: |
@@ -121,18 +104,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Compile release sources - name: Compile release sources
if: ${{ matrix.variant == 'release' }} if: ${{ matrix.variant == 'release' }}
run: | run: ./gradlew bundleGplayRelease -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
./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
- name: Compile nightly sources - name: Compile nightly sources
if: ${{ matrix.variant == 'nightly' }} if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
+7 -21
View File
@@ -7,28 +7,17 @@ on:
push: push:
branches: [ develop ] branches: [ develop ]
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
build: build:
name: Build Enterprise APKs name: Build Enterprise APKs
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Skip in forks # Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
strategy: strategy:
@@ -41,9 +30,6 @@ jobs:
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -57,7 +43,7 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -70,12 +56,12 @@ jobs:
- name: Clone submodules - name: Clone submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug Gplay Enterprise APK - name: Assemble debug Gplay Enterprise APK
@@ -93,7 +79,7 @@ jobs:
run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:assembleGplayDebug -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Upload debug Enterprise APKs - name: Upload debug Enterprise APKs
if: ${{ matrix.variant == 'debug' }} if: ${{ matrix.variant == 'debug' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: elementx-enterprise-debug name: elementx-enterprise-debug
path: | path: |
+33
View File
@@ -0,0 +1,33 @@
name: Danger CI
on: [pull_request, merge_group]
permissions: {}
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 - name: ⏬ Checkout with LFS
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.14 python-version: 3.14
- name: Run World screenshots generation script - name: Run World screenshots generation script
+3 -2
View File
@@ -13,11 +13,12 @@ jobs:
# Skip in forks # Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
name: Use JDK 21 name: Use JDK 21
if: (github.event_name == 'pull_request' && github.event.pull_request.fork == null) || github.event_name == 'workflow_dispatch'
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
@@ -46,26 +46,19 @@ concurrency:
env: env:
# Upstream's workflows hardcode -Xmx8g / Kotlin daemon -Xmx4g, sized for the # Upstream's workflows hardcode -Xmx8g / Kotlin daemon -Xmx4g, sized for the
# 4-core/16GB ubuntu-latest that public repos get. On the previous incus # 4-core/16GB ubuntu-latest that public repos get. On this container the
# container the daemon kept getting SIGKILLed mid-build # daemon keeps getting SIGKILLed mid-build (DaemonDisappearedException, no
# (DaemonDisappearedException, no OutOfMemoryError, no heap dump), so these # OutOfMemoryError, no heap dump), so stay well under upstream's ceiling and
# values stay well under upstream's ceiling and leave room for the Kotlin # leave room for the Kotlin daemon and the parallel lint workers.
# daemon and the parallel lint workers. Re-check them against the "Report
# runner capacity" output now that jobs land on the persistent org runners,
# whose specs may differ.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
release: release:
name: Build and upload AAB to Play Store name: Build and upload AAB to Play Store
# Persistent org runners (github-runner-01, gha-runner-1..3). The previous runs-on: novaloop-incus-large
# novaloop-incus-large label pointed at the GARM ephemeral pool, whose freshly
# spawned runners were reaped mid-job — jobs died ~10 minutes in with
# "The operation was canceled" and no other diagnostic.
runs-on: [self-hosted, Linux, X64]
steps: steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
@@ -85,13 +78,10 @@ jobs:
- name: Resolve marketing version - name: Resolve marketing version
id: version id: version
env:
# Passed through the environment, never expanded into the script (zizmor: template-injection).
INPUT_VERSION: ${{ github.event.inputs.version }}
run: | run: |
set -euo pipefail set -euo pipefail
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION" VERSION="${{ github.event.inputs.version }}"
else else
VERSION="${GITHUB_REF_NAME#healthchat-v}" VERSION="${GITHUB_REF_NAME#healthchat-v}"
fi fi
@@ -181,24 +171,25 @@ jobs:
done done
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '21'
# The Android SDK now comes from the self-hosted runner image, so there is no # The self-hosted runner image has no Android SDK (unlike GitHub-hosted
# setup-android step here. The image must provide it AND export ANDROID_HOME to # ubuntu-latest), so install it here.
# the job environment — Gradle fails with "SDK location not found" otherwise. # - platform: follows COMPILE_SDK in plugins/src/main/kotlin/Versions.kt.
# The image also has to carry the platform matching COMPILE_SDK in # Platform packages are minor-versioned, so compileSdk 37 is android-37.0.
# plugins/src/main/kotlin/Versions.kt (platform packages are minor-versioned, so # - build-tools: nothing sets buildToolsVersion, so AGP picks its own
# compileSdk 37 means android-37.0) and the build-tools version AGP defaults to # default (SdkConstants.CURRENT_BUILD_TOOLS_VERSION). Bump it when AGP is
# (SdkConstants.CURRENT_BUILD_TOOLS_VERSION), since nothing sets buildToolsVersion # upgraded, not when Versions.kt changes.
# here. Both need refreshing in the image when AGP or COMPILE_SDK moves. - name: Set up Android SDK
- name: Configure gradle uses: android-actions/setup-android@v3
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
with: with:
# Release builds must not consume a cache another workflow could have poisoned. packages: 'platforms;android-37.0 build-tools;36.0.0'
cache-disabled: true
- name: Configure gradle
uses: gradle/actions/setup-gradle@v4
- name: Decode keystore - name: Decode keystore
run: | run: |
@@ -228,7 +219,7 @@ jobs:
- name: Upload workflow artifacts - name: Upload workflow artifacts
if: always() if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@v4
with: with:
name: healthchat-${{ steps.version.outputs.version }} name: healthchat-${{ steps.version.outputs.version }}
path: | path: |
@@ -238,7 +229,7 @@ jobs:
if-no-files-found: warn if-no-files-found: warn
- name: Upload to Play Store - name: Upload to Play Store
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5 uses: r0adkll/upload-google-play@v1
with: with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }} serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
packageName: ch.healthchat.element packageName: ch.healthchat.element
@@ -247,8 +238,4 @@ jobs:
status: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_status || 'completed' }} status: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_status || 'completed' }}
whatsNewDirectory: fastlane/metadata/android whatsNewDirectory: fastlane/metadata/android
mappingFile: app/build/outputs/mapping/gplayRelease/mapping.txt mappingFile: app/build/outputs/mapping/gplayRelease/mapping.txt
# Do NOT set changesNotSentForReview. Play now sends this app's changes for review changesNotSentForReview: true
# automatically, and the Edits API rejects the parameter outright when it does:
# "Changes are sent for review automatically. The query parameter
# changesNotSentForReview must not be set." The AAB uploads fine and then the commit
# fails, so the release silently never lands on the track. See SKILL.md.
+11 -44
View File
@@ -5,22 +5,11 @@ on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
ARCH: x86_64 ARCH: x86_64
DEVICE: pixel_7_pro DEVICE: pixel_7_pro
@@ -30,18 +19,12 @@ env:
jobs: jobs:
build-apk: build-apk:
name: Build APK name: Build APK
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# 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: concurrency:
group: ${{ format('maestro-build-{0}', github.ref) }} group: ${{ format('maestro-build-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -55,19 +38,19 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.ref }} ref: ${{ github.ref }}
persist-credentials: false persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
name: Use JDK 21 name: Use JDK 21
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Assemble debug APK - name: Assemble debug APK
@@ -77,7 +60,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }} ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID: ${{ secrets.MAPTILER_LIGHT_MAP_ID }}
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
- name: Upload APK as artifact - name: Upload APK as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: elementx-apk-maestro name: elementx-apk-maestro
path: | path: |
@@ -90,16 +73,13 @@ jobs:
name: Maestro test suite name: Maestro test suite
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ build-apk ] 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. # Allow only one to run at a time, since they use the same environment.
# Otherwise, tests running in parallel can break each other. # 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: concurrency:
group: maestro-test group: maestro-test
queue: max
steps: 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: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -118,7 +98,7 @@ jobs:
run: curl -fsSL "https://get.maestro.mobile.dev" | bash run: curl -fsSL "https://get.maestro.mobile.dev" | bash
- name: Run Maestro tests in emulator - name: Run Maestro tests in emulator
id: maestro_test 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 continue-on-error: true
env: env:
MAESTRO_USERNAME: maestroelement MAESTRO_USERNAME: maestroelement
@@ -138,21 +118,8 @@ jobs:
disk-size: 3G disk-size: 3G
script: | script: |
.github/workflows/scripts/maestro/maestro-local-with-screen-recording.sh app-gplay-x86_64-debug.apk .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 - name: Upload test results
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: test-results name: test-results
path: | path: |
+2 -2
View File
@@ -32,11 +32,11 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
+9 -9
View File
@@ -37,28 +37,28 @@ jobs:
uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: false cache-read-only: false
- name: ⚙️ Run unit tests, debug and release - 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 - 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 - name: 📈 Generate kover report and verify coverage
run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES run: ./gradlew :app:koverXmlReportGplayDebug :app:koverHtmlReportGplayDebug :app:koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- name: ✅ Upload kover report - name: ✅ Upload kover report
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: kover-results name: kover-results
path: | path: |
@@ -76,23 +76,23 @@ jobs:
name: Dependency analysis name: Dependency analysis
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Dependency analysis - name: Dependency analysis
run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES run: ./gradlew dependencyCheckAnalyze $CI_GRADLE_ARG_PROPERTIES
- name: Upload dependency analysis - name: Upload dependency analysis
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: dependency-analysis name: dependency-analysis
path: build/reports/dependency-check-report.html 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.`);
}
+27 -32
View File
@@ -23,38 +23,33 @@ jobs:
script: | script: |
core.setFailed("PR has been labeled with X-Blocked; it cannot be merged."); core.setFailed("PR has been labeled with X-Blocked; it cannot be merged.");
# Disabled on the HealthChat fork. This job asks whether the PR author belongs to the community-prs:
# element-hq "Vector Core" team, using secrets.ELEMENT_BOT_TOKEN_READ_ORG — a secret name: Label Community PRs
# that only exists upstream. Here it fails on every PR that is opened, and there is no runs-on: ubuntu-latest
# community to label anyway. Kept commented rather than deleted so upstream changes to if: github.event.action == 'opened'
# it still conflict visibly on merge. permissions:
# community-prs: pull-requests: write
# name: Label Community PRs steps:
# runs-on: ubuntu-latest - name: Check membership
# if: github.event.action == 'opened' if: github.event.pull_request.user.login != 'renovate[bot]'
# permissions: uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2
# pull-requests: write id: teams
# steps: with:
# - name: Check membership username: ${{ github.event.pull_request.user.login }}
# if: github.event.pull_request.user.login != 'renovate[bot]' organization: element-hq
# uses: tspascoal/get-user-teams-membership@b2546c5affc730fd8e3d8483ae9ad3621938c2f9 # v4.0.2 team: Vector Core
# id: teams GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }}
# with: - name: Add label
# username: ${{ github.event.pull_request.user.login }} if: steps.teams.outputs.isTeamMember == 'false'
# organization: element-hq uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# team: Vector Core with:
# GITHUB_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN_READ_ORG }} script: |
# - name: Add label github.rest.issues.addLabels({
# if: steps.teams.outputs.isTeamMember == 'false' issue_number: context.issue.number,
# uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 owner: context.repo.owner,
# with: repo: context.repo.repo,
# script: | labels: ['Z-Community-PR']
# github.rest.issues.addLabels({ });
# issue_number: context.issue.number,
# owner: context.repo.owner,
# repo: context.repo.repo,
# labels: ['Z-Community-PR']
# });
close-if-fork-develop: close-if-fork-develop:
name: Forbid develop branch fork contributions name: Forbid develop branch fork contributions
+61 -226
View File
@@ -7,22 +7,11 @@ on:
push: push:
branches: [ main, develop ] branches: [ main, develop ]
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
jobs: jobs:
@@ -31,9 +20,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -47,7 +33,7 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Add SSH private keys for submodule repositories - name: Add SSH private keys for submodule repositories
@@ -65,11 +51,11 @@ jobs:
name: Search for invalid screenshot files name: Search for invalid screenshot files
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.14 python-version: 3.14
- name: Search for invalid screenshot files - name: Search for invalid screenshot files
@@ -77,22 +63,22 @@ jobs:
checkDependencies: checkDependencies:
name: Search for invalid dependencies name: Search for invalid dependencies
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.14 python-version: 3.14
- name: Search for invalid dependencies - name: Search for invalid dependencies
@@ -101,13 +87,13 @@ jobs:
# Code checks # Code checks
konsist: konsist:
name: Konsist tests name: Konsist tests
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
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) }} 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 cancel-in-progress: true
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -122,19 +108,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Konsist tests - name: Run Konsist tests
run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon run: ./gradlew :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --no-daemon
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: konsist-report name: konsist-report
path: | path: |
@@ -142,22 +128,13 @@ jobs:
compose: compose:
name: Compose tests name: Compose tests
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('check-compose-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('check-compose-develop-{0}', github.sha) || format('check-compose-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
# Records the container's real limits so heap settings can be read against them - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# instead of guessed at. Mirrors healthchat-release-playstore.yml.
- name: Report runner capacity
if: runner.environment == 'self-hosted'
run: |
free -h; nproc; df -h .
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
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -172,12 +149,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run compose tests - name: Run compose tests
@@ -185,13 +162,13 @@ jobs:
lint: lint:
name: Android lint check name: Android lint check
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
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) }} 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 cancel-in-progress: true
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -206,12 +183,12 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build Gplay Debug - name: Build Gplay Debug
@@ -222,7 +199,7 @@ jobs:
run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue run: ./gradlew :app:lintGplayDebug :app:lintFdroidDebug lintDebug $CI_GRADLE_ARG_PROPERTIES --continue
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: linting-report name: linting-report
path: | path: |
@@ -230,13 +207,13 @@ jobs:
detekt: detekt:
name: Detekt checks name: Detekt checks
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
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) }} 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 cancel-in-progress: true
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -251,19 +228,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Detekt - 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 - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: detekt-report name: detekt-report
path: | path: |
@@ -271,13 +248,13 @@ jobs:
ktlint: ktlint:
name: Ktlint checks name: Ktlint checks
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
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) }} 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 cancel-in-progress: true
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -292,19 +269,19 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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: git submodule update --init --recursive
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Run Ktlint check - name: Run Ktlint check
run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES --continue run: ./gradlew ktlintCheck $CI_GRADLE_ARG_PROPERTIES
- name: Upload reports - name: Upload reports
if: always() if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: ktlint-report name: ktlint-report
path: | path: |
@@ -318,7 +295,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) }} 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 cancel-in-progress: true
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
@@ -342,7 +319,7 @@ jobs:
name: Check shell scripts name: Check shell scripts
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Run shellcheck - name: Run shellcheck
@@ -354,181 +331,39 @@ jobs:
name: Run zizmor name: Run zizmor
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
# A job-level block replaces the workflow-level one, so contents:read has to be
# repeated here or checkout fails on this private fork.
contents: read
actions: read # Required for zizmor to read workflow files.
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 - uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
upload_reports: upload_reports:
name: Project Check Suite name: Project Check Suite
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [konsist, lint, ktlint, detekt] needs: [konsist, lint, ktlint, detekt]
# Run even if a previous job failed: that is precisely when there are issues to report. if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
if: ${{ !cancelled() }}
permissions:
# Required to list the files modified by the pull request.
pull-requests: read
steps: 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 - name: Download reports from previous jobs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- name: Annotate the lint, ktlint and detekt issues - name: Prepare Danger
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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: with:
script: | args: "--dangerfile ./tools/danger/dangerfile-lint.js"
// GitHub only surfaces a handful of annotations per level and per step, so cap what we emit env:
// and let the job summary carry the full list. DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
const MAX_ANNOTATIONS = 20; # Fallback for forks
const MAX_SUMMARY_ROWS = 200; GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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();
+11 -37
View File
@@ -5,27 +5,12 @@ on:
pull_request: pull_request:
types: [ labeled ] types: [ labeled ]
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g -Dsonar.gradle.skipCompile=true
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room — CI_GRADLE_ARG_PROPERTIES: --no-configuration-cache
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
# Kept character-identical to tests.yml so the record path and the verify path cannot drift
# apart again. These only affect the Gradle daemon — Paparazzi renders in forked test workers
# configured entirely by build.gradle.kts — but keeping them aligned removes a whole class of
# "did record and verify run the same way?" questions.
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache --max-workers=4
jobs: jobs:
record: record:
@@ -34,14 +19,11 @@ jobs:
pull-requests: write pull-requests: write
contents: write contents: write
name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }} name: Record screenshots on branch ${{ github.event.pull_request.head.ref || github.ref_name }}
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots' if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'Record-Screenshots'
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -60,29 +42,25 @@ jobs:
uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0
with: with:
labels: Record-Screenshots 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) - name: ⏬ Checkout with LFS (PR)
if: github.event.label.name == 'Record-Screenshots' 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: with:
persist-credentials: true persist-credentials: false
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
- name: ⏬ Checkout with LFS (Branch) - name: ⏬ Checkout with LFS (Branch)
if: github.event_name == 'workflow_dispatch' 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: with:
persist-credentials: true persist-credentials: false
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
# Add gradle cache, this should speed up the process # Add gradle cache, this should speed up the process
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Record screenshots - name: Record screenshots
@@ -90,9 +68,5 @@ jobs:
run: ./.github/workflows/scripts/recordScreenshots.sh run: ./.github/workflows/scripts/recordScreenshots.sh
env: env:
GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN || secrets.GITHUB_TOKEN }}
# Must be the `github.repository` context, not a secret. There is no GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }}
# GITHUB_REPOSITORY secret on this fork, so `secrets.GITHUB_REPOSITORY` expands to ""
# and overrides the built-in env var of the same name — recordScreenshots.sh then
# exits 1 with "No repo specified" before Gradle ever runs.
GITHUB_REPOSITORY: ${{ github.repository }}
GRADLE_ARGS: ${{ env.CI_GRADLE_ARG_PROPERTIES }} GRADLE_ARGS: ${{ env.CI_GRADLE_ARG_PROPERTIES }}
+46 -12
View File
@@ -34,18 +34,16 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false 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 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - 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 - name: Create app bundle
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -59,12 +57,50 @@ jobs:
ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }} ELEMENT_CALL_RAGESHAKE_URL: ${{ secrets.ELEMENT_CALL_RAGESHAKE_URL }}
run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES run: ./gradlew bundleGplayRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload bundle as artifact - name: Upload bundle as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: elementx-app-gplay-bundle-unsigned name: elementx-app-gplay-bundle-unsigned
path: | path: |
app/build/outputs/bundle/gplayRelease/app-gplay-release.aab 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: fdroid:
name: Create APKs (FDroid) name: Create APKs (FDroid)
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -86,18 +122,16 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false 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 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Create APKs - name: Create APKs
env: env:
ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }}
@@ -105,7 +139,7 @@ jobs:
ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }} ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID: ${{ secrets.MAPTILER_DARK_MAP_ID }}
run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES run: ./gradlew assembleFdroidRelease $CI_GRADLE_ARG_PROPERTIES
- name: Upload apks as artifact - name: Upload apks as artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: elementx-app-fdroid-apks-unsigned name: elementx-app-fdroid-apks-unsigned
path: | path: |
@@ -51,13 +51,18 @@ if [[ -z ${REPO} ]]; then
exit 1 exit 1
fi fi
# Note: the previous screenshots are deliberately kept, so that Paparazzi can leave the ones that only echo "Deleting previous screenshots"
# differ by less than `maxPercentDifference` untouched instead of rewriting their Git LFS blob. The ./gradlew removeOldSnapshots --stacktrace --warn $GRADLE_ARGS
# screenshots of previews that no longer exist are pruned afterwards by `pruneObsoleteSnapshots`, which
# `recordPaparazziDebug` is finalized by.
echo "Record screenshots" echo "Record screenshots"
./gradlew recordPaparazziDebug --stacktrace $GRADLE_ARGS ./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" echo "Committing changes"
git config http.sslVerify false git config http.sslVerify false
@@ -73,10 +78,6 @@ else
git config --local user.name "${INPUT_AUTHOR_EMAIL}" git config --local user.name "${INPUT_AUTHOR_EMAIL}"
fi fi
git add -A 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" git commit -m "Update screenshots"
GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git" GITHUB_REPO="https://$GITHUB_ACTOR:$TOKEN@github.com/$REPO.git"
+6 -20
View File
@@ -7,38 +7,24 @@ on:
push: push:
branches: [ main, develop ] branches: [ main, develop ]
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx4g
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room —
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -Dkotlin.daemon.jvm.options=-Xmx2g
CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache CI_GRADLE_ARG_PROPERTIES: --stacktrace --warn -Dsonar.gradle.skipCompile=true --no-configuration-cache
GROUP: ${{ format('sonar-{0}', github.ref) }} GROUP: ${{ format('sonar-{0}', github.ref) }}
jobs: jobs:
sonar: sonar:
name: Sonar Quality Checks name: Sonar Quality Checks
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
group: ${{ format('sonar-{0}', github.ref) }} group: ${{ format('sonar-{0}', github.ref) }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
steps: steps:
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -52,19 +38,19 @@ jobs:
docker-images: true docker-images: true
swap-storage: false swap-storage: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
# Ensure we are building the branch and not the branch after being merged on develop # Ensure we are building the branch and not the branch after being merged on develop
# https://github.com/actions/checkout/issues/881 # https://github.com/actions/checkout/issues/881
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Build debug code and test fixtures - name: Build debug code and test fixtures
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
with: with:
only-labels: "X-Needs-Info" only-labels: "X-Needs-Info"
days-before-issue-stale: 30 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: {} 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: jobs:
sync-localazy: sync-localazy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip in forks # Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }}
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Use JDK 21 - name: Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.14 python-version: 3.14
- name: Setup Localazy - name: Setup Localazy
@@ -42,7 +39,6 @@ jobs:
./tools/localazy/downloadStrings.sh --all ./tools/localazy/downloadStrings.sh --all
./tools/localazy/importSupportedLocalesFromLocalazy.py ./tools/localazy/importSupportedLocalesFromLocalazy.py
./tools/test/generateAllScreenshots.py ./tools/test/generateAllScreenshots.py
./tools/manifest/aaptDump.sh
- name: Create Pull Request for Strings - name: Create Pull Request for Strings
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with: 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' }} 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. # No concurrency required, runs every time on a schedule.
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Python 3.12 - name: Set up Python 3.12
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with: with:
python-version: 3.14 python-version: 3.14
- name: Install Prerequisite dependencies - name: Install Prerequisite dependencies
+13 -57
View File
@@ -7,53 +7,24 @@ on:
push: push:
branches: [ main, develop ] branches: [ main, develop ]
permissions: 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
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
# Reduced from upstream's -Xmx8g / 4 GB Kotlin daemon for the self-hosted runners. GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx7g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
# Each incus container reports 48 GiB and 8 cores, so a single build has ample room — CI_GRADLE_ARG_PROPERTIES: --stacktrace --no-daemon -Dsonar.gradle.skipCompile=true --no-configuration-cache
# but the containers are oversubscribed against their host, and two concurrent heavy
# Gradle jobs get their daemons SIGKILLed ("the daemon has disappeared", no
# OutOfMemoryError, no heap dump). Running the same job alone succeeds. Lower heaps
# make two builds likelier to coexist; the real fix is sizing the containers to the
# host. See the "Report runner capacity" step.
GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx6g -XX:MaxMetaspaceSize=768m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options=-Xmx2g -XX:+UseG1GC
# --max-workers caps how many test tasks run at once. build.gradle.kts sets
# maxParallelForks = cores/2 and screenshot tests fork at maxHeapSize=2g, so on these
# 8-core containers the default (workers = cores = 8) allows 8 x 4 x 2g = 64 GB of test
# 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
jobs: jobs:
tests: tests:
name: Runs unit tests name: Runs unit tests
runs-on: [self-hosted, Linux, X64] runs-on: ubuntu-latest
# Allow all jobs on main and develop. Just one per PR. # Allow all jobs on main and develop. Just one per PR.
concurrency: concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }} group: ${{ github.ref == 'refs/heads/main' && format('unit-tests-main-{0}', github.sha) || github.ref == 'refs/heads/develop' && format('unit-tests-develop-{0}', github.sha) || format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
# Records the container's real limits so heap settings can be read against them
# instead of guessed at. Mirrors healthchat-release-playstore.yml.
- name: Report runner capacity
if: runner.environment == 'self-hosted'
run: |
free -h; nproc; df -h .
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"
- name: Free Disk Space (Ubuntu) - name: Free Disk Space (Ubuntu)
# Only meaningful on GitHub's throwaway VMs. On a persistent self-hosted runner this
# would delete dotnet/haskell/docker images from the host on every run.
if: runner.environment == 'github-hosted'
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with: with:
# This might remove tools that are actually needed, if set to "true" but frees about 6 GB # This might remove tools that are actually needed, if set to "true" but frees about 6 GB
@@ -70,9 +41,6 @@ jobs:
# Increase swapfile size to prevent screenshot tests getting terminated # Increase swapfile size to prevent screenshot tests getting terminated
# https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749 # https://github.com/actions/runner-images/discussions/7188#discussioncomment-6750749
- name: 💽 Increase swapfile size - name: 💽 Increase swapfile size
# /mnt/swapfile is a GitHub runner-image path and this needs passwordless sudo,
# so it only applies to the hosted VMs.
if: runner.environment == 'github-hosted'
run: | run: |
sudo swapoff -a sudo swapoff -a
sudo fallocate -l 8G /mnt/swapfile sudo fallocate -l 8G /mnt/swapfile
@@ -95,31 +63,25 @@ jobs:
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'element-hq/element-x-android' }} 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: git submodule update --init --recursive
- name: ☕️ Use JDK 21 - name: ☕️ Use JDK 21
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with: with:
distribution: 'temurin' # See 'Supported distributions' for available options distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21' java-version: '21'
- name: Configure gradle - name: Configure gradle
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
with: with:
cache-read-only: ${{ github.ref != 'refs/heads/develop' }} cache-read-only: ${{ github.ref != 'refs/heads/develop' }}
- name: ⚙️ Run unit tests - name: ⚙️ Check coverage for debug variant (includes unit & screenshot tests)
run: ./gradlew :app:testGplayDebugUnitTest testDebugUnitTest -x :tests:konsist:testDebugUnitTest $CI_GRADLE_ARG_PROPERTIES --continue run: ./gradlew testDebugUnitTest :tests:uitests:verifyPaparazziDebug :koverXmlReportMerged :koverHtmlReportMerged :koverVerifyAll $CI_GRADLE_ARG_PROPERTIES
- 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: 🚫 Upload kover failed coverage reports - name: 🚫 Upload kover failed coverage reports
if: failure() if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: kover-error-report name: kover-error-report
path: | path: |
build/reports/kover app/build/reports/kover
- name: ✅ Upload kover report (disabled) - name: ✅ Upload kover report (disabled)
if: always() if: always()
@@ -127,11 +89,12 @@ jobs:
- name: 🚫 Upload test results on error - name: 🚫 Upload test results on error
if: failure() if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: tests-and-screenshot-tests-results name: tests-and-screenshot-tests-results
path: | path: |
**/build/paparazzi/failures/ **/build/paparazzi/failures/
**/build/roborazzi/failures/
**/build/reports/tests/*UnitTest/ **/build/reports/tests/*UnitTest/
- name: 🚫 Modify summary on error - name: 🚫 Modify summary on error
@@ -144,15 +107,8 @@ jobs:
echo "---" >> $GITHUB_STEP_SUMMARY echo "---" >> $GITHUB_STEP_SUMMARY
# https://github.com/codecov/codecov-action # https://github.com/codecov/codecov-action
# This fork has no CODECOV_TOKEN, and Codecov rejects tokenless uploads from private
# repositories ("Token required - not valid tokenless upload"). With
# fail_ci_if_error: true that failed the whole job *after* the tests had passed, so
# skip the upload unless a token is configured. Adding the secret re-enables it.
- name: ☂️ Upload coverage reports to codecov - name: ☂️ Upload coverage reports to codecov
if: env.CODECOV_TOKEN != '' uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+1 -5
View File
@@ -2,11 +2,7 @@ name: Validate Git LFS
on: [pull_request, merge_group] on: [pull_request, merge_group]
permissions: 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: jobs:
build: build:
+3
View File
@@ -31,6 +31,9 @@ __pycache__/
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files # Log Files
*.log *.log
-34
View File
@@ -1,39 +1,5 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <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> <JetCodeStyleSettings>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" /> <option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+1 -1
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager"> <component name="CopyrightManager">
<copyright> <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" /> <option name="myName" value="Element Enterprise" />
</copyright> </copyright>
</component> </component>
+1
View File
@@ -16,6 +16,7 @@
<w>posthog</w> <w>posthog</w>
<w>rageshake</w> <w>rageshake</w>
<w>securebackup</w> <w>securebackup</w>
<w>showkase</w>
<w>snackbar</w> <w>snackbar</w>
<w>spdx</w> <w>spdx</w>
<w>swipeable</w> <w>swipeable</w>
+2 -2
View File
@@ -2,6 +2,6 @@
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" /> <option name="externalSystemId" value="Gradle" />
<option name="version" value="2.4.20" /> <option name="version" value="2.4.0" />
</component> </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" - tapOn: "Sign in manually"
- runFlow: ../assertions/assertLoginDisplayed.yaml - runFlow: ../assertions/assertLoginDisplayed.yaml
- takeScreenshot: build/maestro/100-SignIn - 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: - tapOn:
id: "login-continue" id: "login-continue"
## MAS page ## 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: - retry:
maxRetries: 3 maxRetries: 3
commands: commands:
+1 -10
View File
@@ -6,19 +6,10 @@ appId: ${MAESTRO_APP_ID}
- takeScreenshot: build/maestro/900-SignOutScreen - takeScreenshot: build/maestro/900-SignOutScreen
- back - back
- tapOn: "Remove this device" - tapOn: "Remove this device"
# Ensure pressing back cancels
- back
- tapOn: "Remove this device"
# Ensure cancel cancels # Ensure cancel cancels
- tapOn: "Cancel"
- tapOn: "Remove this device"
- tapOn:
id: "sign-out-submit"
# Cancel the dialog cancels the logout
- tapOn: - tapOn:
id: "dialog-negative" id: "dialog-negative"
- tapOn: - tapOn: "Remove this device"
id: "sign-out-submit"
- tapOn: - tapOn:
id: "dialog-positive" id: "dialog-positive"
- runFlow: ../assertions/assertInitDisplayed.yaml - runFlow: ../assertions/assertInitDisplayed.yaml
+1 -1
View File
@@ -16,4 +16,4 @@ appId: ${MAESTRO_APP_ID}
- extendedWaitUntil: - extendedWaitUntil:
visible: "Device verified" visible: "Device verified"
timeout: 30000 timeout: 30000
- tapOn: "Done" - tapOn: "Continue"
@@ -1,5 +1,5 @@
appId: ${MAESTRO_APP_ID} appId: ${MAESTRO_APP_ID}
--- ---
- extendedWaitUntil: - extendedWaitUntil:
visible: "Enter your account provider" visible: "Change account provider"
timeout: 10000 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: - tapOn:
text: ${MAESTRO_INVITEE1_MXID} text: ${MAESTRO_INVITEE1_MXID}
index: 1 index: 1
- runFlow: - tapOn: "Continue"
when:
visible: 'Start a chat with this new contact?'
commands:
- tapOn: 'Continue'
- runFlow:
when:
visible: 'Send invite?'
commands:
- tapOn: 'Send invite'
- takeScreenshot: build/maestro/330-createAndDeleteDM - takeScreenshot: build/maestro/330-createAndDeleteDM
- tapOn: "maestroelement2" - tapOn: "maestroelement2"
- scroll - scroll
@@ -8,12 +8,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn: "Add description…" - tapOn: "Add description…"
- inputText: "aRoomTopic" - inputText: "aRoomTopic"
- tapOn: "Create" - tapOn: "Create"
# It can take ages to create room on matrix.org
- extendedWaitUntil:
timeout: 30000
visible: "Invite people"
- takeScreenshot: build/maestro/320-createAndDeleteRoom - takeScreenshot: build/maestro/320-createAndDeleteRoom
- tapOn: "Search for someone" - tapOn: "Search for someone"
- inputText: ${MAESTRO_INVITEE1_MXID} - inputText: ${MAESTRO_INVITEE1_MXID}
@@ -21,12 +15,6 @@ appId: ${MAESTRO_APP_ID}
text: ${MAESTRO_INVITEE1_MXID} text: ${MAESTRO_INVITEE1_MXID}
index: 1 index: 1
- tapOn: "Finish" - tapOn: "Finish"
- runFlow:
when:
visible: 'Invite new contact to this room?'
commands:
- tapOn:
id: "confirm_invite_unknown"
- tapOn: "aRoomName" - tapOn: "aRoomName"
- tapOn: "Invite" - tapOn: "Invite"
# assert there's 1 member and 1 invitee # assert there's 1 member and 1 invitee
@@ -35,10 +23,6 @@ appId: ${MAESTRO_APP_ID}
- tapOn: - tapOn:
text: ${MAESTRO_INVITEE2_MXID} text: ${MAESTRO_INVITEE2_MXID}
index: 1 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" - tapOn: "Invite"
- runFlow: - runFlow:
when: when:
@@ -46,6 +30,10 @@ appId: ${MAESTRO_APP_ID}
commands: commands:
- tapOn: - tapOn:
id: "confirm_invite_unknown" id: "confirm_invite_unknown"
# Close the keyboard if it's still open
- tapOn: "Back"
# Go back to the room details screen
- tapOn: "Back"
- scrollUntilVisible: - scrollUntilVisible:
direction: DOWN direction: DOWN
element: element:
+1 -2
View File
@@ -1,7 +1,6 @@
appId: ${MAESTRO_APP_ID} appId: ${MAESTRO_APP_ID}
--- ---
- runFlow: cleanUpRoomList.yaml - runFlow: searchRoomList.yaml
#- runFlow: searchRoomList.yaml
- takeScreenshot: build/maestro/300-RoomList - takeScreenshot: build/maestro/300-RoomList
- runFlow: timeline/timeline.yaml - runFlow: timeline/timeline.yaml
- runFlow: roomContextMenu.yaml - runFlow: roomContextMenu.yaml
@@ -3,5 +3,5 @@ appId: ${MAESTRO_APP_ID}
- takeScreenshot: build/maestro/520-Timeline - takeScreenshot: build/maestro/520-Timeline
- tapOn: "Add attachment" - tapOn: "Add attachment"
- tapOn: "Share location" - tapOn: "Share location"
- tapOn: "Share my location" - tapOn: "Share selected location"
- takeScreenshot: build/maestro/521-Timeline - takeScreenshot: build/maestro/521-Timeline
+8 -61
View File
@@ -4,12 +4,9 @@ appId: ${MAESTRO_APP_ID}
id: "home_screen-settings" id: "home_screen-settings"
- assertVisible: "Settings" - assertVisible: "Settings"
- takeScreenshot: build/maestro/600-Settings - takeScreenshot: build/maestro/600-Settings
## Account settings
- tapOn: - tapOn:
id: "settings-user_profile" text: "Analytics"
- assertVisible: "Edit profile" - assertVisible: "Share analytics data"
- takeScreenshot: build/maestro/610-AccountEditProfile
- back - back
- tapOn: - tapOn:
@@ -17,27 +14,16 @@ appId: ${MAESTRO_APP_ID}
- assertVisible: "Enable notifications on this device" - assertVisible: "Enable notifications on this device"
- back - back
- tapOn:
text: "Encryption"
- assertVisible: "Key storage"
- back
- tapOn:
text: "Moderation & safety"
- assertVisible: "Your appearance"
- back
- tapOn: - tapOn:
text: "Report a problem" text: "Report a problem"
- assertVisible: "Report a problem" - assertVisible: "Report a problem"
- back - back
## App settings section
- assertVisible: "Appearance"
- tapOn: - tapOn:
text: "Media upload quality" text: "About"
- assertVisible: "Media upload quality" - assertVisible: "Copyright"
- assertVisible: "Acceptable use policy"
- assertVisible: "Privacy policy"
- back - back
- tapOn: - tapOn:
@@ -46,53 +32,14 @@ appId: ${MAESTRO_APP_ID}
- hideKeyboard - hideKeyboard
- back - back
- scrollUntilVisible:
direction: DOWN
element:
text: "Location sharing"
- tapOn: - tapOn:
text: "Location sharing" text: "Advanced settings"
- assertVisible: "Live location updates" - assertVisible: "View source"
- back - 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: - tapOn:
text: "Developer options" text: "Developer options"
- assertVisible: "Feature flags" - assertVisible: "Feature flags"
- assertVisible: "View source"
- back - back
- back - back
+3 -5
View File
@@ -37,9 +37,8 @@ PRs must meet these rules.
- Create previews for **all main states** of a Composable. - Create previews for **all main states** of a Composable.
- Use `@PreviewsDayNight` for consistency. - 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 { ... }`. - 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 #…". - Title = changelog entry — descriptive, no "Fixes #…".
- Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette). - Leave description template for the developer. Redirect them to the [contributing etiquette](CONTRIBUTING.md#etiquette).
- Screenshots/videos for visual changes. - 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. - Commits need a title and description; no tiny or massive commits.
- No history rewrites. - 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`. | | `FooView.kt` | Stateless Composable rendering the UI from `FooState`. |
| `FooState.kt` | Data class representing the immutable UI state. | | `FooState.kt` | Data class representing the immutable UI state. |
| `FooEvent.kt` | Sealed interface for UI actions sent to the Presenter. | | `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. | | `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 Changes in Element X v26.06.4
============================= =============================
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem 'danger'
-33
View File
@@ -6,39 +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 form `## Changes in <version> (<date>)` and the whole section must stay under
500 characters. See `.claude/skills/releasing-healthchat/SKILL.md`. 500 characters. See `.claude/skills/releasing-healthchat/SKILL.md`.
## Changes in 26.10.1 (2026-10-21)
🐛 Fehlerbehebungen
- Anrufe aus der Android-App lassen das iPhone wieder klingeln.
- Abgebrochene Anrufe werden auf Android wieder sofort beendet.
## 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
- Mitgliederverzeichnis: Alle Mitglieder durchsuchen und direkt einen Chat starten.
- Profil bearbeiten: Angaben und Profilbild direkt in der App pflegen.
- Neue Mitglieder per E-Mail einladen.
- Statushinweise zeigen den Stand der Kontofreigabe.
🙌 Verbesserungen
- HealthChat-Links öffnen sich neu direkt in der App statt im Browser.
- Geteilte Profillinks verweisen neu auf HealthChat statt auf Element.
## Changes in 26.07.0 (2026-07-16) ## Changes in 26.07.0 (2026-07-16)
🎉 Komplett neue App-Generation 🎉 Komplett neue App-Generation
@@ -14,16 +14,16 @@ import kotlin.reflect.KClass
* Adds Node to the specified component graph. * Adds Node to the specified component graph.
* Equivalent to the following declaration: * Equivalent to the following declaration:
* *
* ```
* @BindingContainer * @BindingContainer
* @ContributesTo(Scope::class) * @ContributesTo(Scope::class)
* interface YourNodeBindingContainer { * abstract class YourNodeModule {
* @Binds
* @IntoMap * @Binds
* @NodeKey(YourNode::class) * @IntoMap
* fun bindYourNodeAssistedFactory(factory: YourNodeAssistedFactory): AssistedNodeFactory<*> * @NodeKey(YourNode::class)
* } * abstract fun bindYourNodeFactory(factory: YourNode.Factory): AssistedNodeFactory<*>
* ``` *}
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
annotation class ContributesNode( annotation class ContributesNode(
+30 -48
View File
@@ -21,7 +21,6 @@ import extension.allFeaturesImpl
import extension.allLibrariesImpl import extension.allLibrariesImpl
import extension.allServicesImpl import extension.allServicesImpl
import extension.buildConfigFieldStr import extension.buildConfigFieldStr
import extension.environmentOrGradleValue
import extension.locales import extension.locales
import extension.setupDependencyInjection import extension.setupDependencyInjection
import extension.testCommonDependencies import extension.testCommonDependencies
@@ -51,9 +50,6 @@ android {
// Keep abiFilter for the universalApk // Keep abiFilter for the universalApk
ndk { ndk {
abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64") abiFilters += listOf("armeabi-v7a", "x86", "arm64-v8a", "x86_64")
// HealthChat fork: ship the native symbol tables (Rust SDK, LiveKit) in the bundle so Play Console
// can symbolicate native crashes and ANRs. Play strips them from the APKs it serves.
debugSymbolLevel = "SYMBOL_TABLE"
} }
// Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split // Ref: https://developer.android.com/studio/build/configure-apk-splits.html#configure-abi-split
@@ -92,14 +88,13 @@ android {
storePassword = "android" storePassword = "android"
} }
register("nightly") { register("nightly") {
keyAlias = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_KEYID", "signing.element.nightly.keyId") keyAlias = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYID")
keyPassword = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD", "signing.element.nightly.keyPassword") ?: project.property("signing.element.nightly.keyId") as? String?
storeFile = file( keyPassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD")
environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_STOREFILE", "signing.element.nightly.storeFile") ?: project.property("signing.element.nightly.keyPassword") as? String?
?.ifBlank { null } storeFile = file("./signature/nightly.keystore")
?: "./signature/nightly.keystore" storePassword = System.getenv("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD")
) ?: project.property("signing.element.nightly.storePassword") as? String?
storePassword = environmentOrGradleValue("ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD", "signing.element.nightly.storePassword")
} }
// HealthChat Play Store upload key. The keystore is not in the repo: // HealthChat Play Store upload key. The keystore is not in the repo:
// CI decodes it from the RELEASE_KEYSTORE_BASE64 secret (see // CI decodes it from the RELEASE_KEYSTORE_BASE64 secret (see
@@ -150,11 +145,24 @@ android {
optimization { optimization {
enable = true enable = true
keepRules { keepRules {
// Equivalent of adding `getDefaultProguardFile("proguard-android-optimize.txt")` (this is the default value). files.add(File(projectDir, "common-proguard-rules.pro"))
includeDefault = true 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.
} }
} }
@@ -239,30 +247,6 @@ androidComponents {
) )
onVariants { variant -> 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 // Assigns a different version code for each output APK
// other than the universal APK. // other than the universal APK.
variant.outputs.forEach { output -> variant.outputs.forEach { output ->
@@ -301,6 +285,7 @@ dependencies {
implementation(projects.features.migration.api) implementation(projects.features.migration.api)
implementation(projects.appnav) implementation(projects.appnav)
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.libraries.uiStrings)
implementation(projects.services.analytics.compose) implementation(projects.services.analytics.compose)
if (ModulesConfig.pushProvidersConfig.includeFirebase) { if (ModulesConfig.pushProvidersConfig.includeFirebase) {
@@ -310,21 +295,22 @@ dependencies {
implementation(projects.libraries.pushproviders.unifiedpush) 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.appyx.core)
implementation(libs.androidx.splash) implementation(libs.androidx.splash)
implementation(libs.androidx.core) implementation(libs.androidx.core)
implementation(libs.androidx.corektx) implementation(libs.androidx.corektx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.startup) implementation(libs.androidx.startup)
implementation(libs.androidx.preference) implementation(libs.androidx.preference)
implementation(libs.coil)
implementation(platform(libs.network.okhttp.bom)) implementation(platform(libs.network.okhttp.bom))
implementation(libs.network.okhttp.logging) implementation(libs.network.okhttp.logging)
implementation(libs.serialization.json)
implementation(libs.matrix.emojibase.bindings)
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
@@ -346,7 +332,6 @@ licensee {
allow("BSD-3-Clause") allow("BSD-3-Clause")
allow("EPL-1.0") allow("EPL-1.0")
allowUrl("https://opensource.org/license/bsd-3-clause") allowUrl("https://opensource.org/license/bsd-3-clause")
allowUrl("https://opensource.org/license/bsd-2-clause")
allowUrl("https://opensource.org/licenses/MIT") allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html") allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/") allowUrl("https://www.zetetic.net/sqlcipher/license/")
@@ -357,9 +342,6 @@ licensee {
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events") ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us. // Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android") 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) { 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;
}
+31 -21
View File
@@ -8,6 +8,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> 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 <application
android:name=".ElementXApplication" android:name=".ElementXApplication"
android:allowBackup="false" android:allowBackup="false"
@@ -49,14 +52,13 @@
Using launchMode singleTask to avoid multiple instances of the Activity Using launchMode singleTask to avoid multiple instances of the Activity
when the app is already open. This is important for incoming share (see when the app is already open. This is important for incoming share (see
https://github.com/element-hq/element-x-android/issues/4074) and for opening https://github.com/element-hq/element-x-android/issues/4074) and for opening
the application from a permalink. the application from a mobile.element.io link.
--> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.ElementX.Splash" android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
@@ -70,7 +72,7 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data <data
android:host="open" android:host="open"
android:scheme="healthchat" /> android:scheme="elementx" />
</intent-filter> </intent-filter>
<!-- <!--
OAuth redirection OAuth redirection
@@ -84,9 +86,7 @@
<data android:scheme="@string/login_redirect_scheme" /> <data android:scheme="@string/login_redirect_scheme" />
</intent-filter> </intent-filter>
<!-- <!--
HealthChat web links Element web links
Note: hosts are listed one by one on purpose, a "*.healthchat.ch" wildcard would
also intercept the homeserver and authentication domains.
--> -->
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -95,12 +95,28 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" /> <data android:scheme="https" />
<!-- HealthChat web client permalinks, example: https://app.healthchat.ch/#/room/#room:healthchat.ch --> <!-- Note: we can't use "*.element.io" here because it'll intercept the "mas.element.io" domain too. -->
<!-- Matching asset file: https://app.healthchat.ch/.well-known/assetlinks.json --> <!-- Matching asset file: https://app.element.io/.well-known/assetlinks.json -->
<data android:host="app.healthchat.ch" /> <data android:host="app.element.io" />
<!-- HealthChat matrix.to deployment, example: https://to.healthchat.ch/#/#room:healthchat.ch --> <!-- Matching asset file: https://develop.element.io/.well-known/assetlinks.json -->
<!-- Matching asset file: https://to.healthchat.ch/.well-known/assetlinks.json --> <data android:host="develop.element.io" />
<data android:host="to.healthchat.ch" /> <!-- Matching asset file: https://staging.element.io/.well-known/assetlinks.json -->
<data android:host="staging.element.io" />
</intent-filter>
<!--
Element mobile links
Example: https://mobile.element.io/element/?account_provider=example.org&login_hint=mxid:@alice:example.org
-->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<!-- Matching asset file: https://mobile.element.io/.well-known/assetlinks.json -->
<data android:host="mobile.element.io" />
<data android:path="/element/" />
</intent-filter> </intent-filter>
<!-- <!--
matrix: links matrix: links
@@ -114,7 +130,7 @@
<data android:scheme="matrix" /> <data android:scheme="matrix" />
</intent-filter> </intent-filter>
<!-- <!--
links from the HealthChat matrix.to website, example: healthchat://room/%23room%3Ahealthchat.ch links from matrix.to website
--> -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -122,16 +138,10 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="healthchat" /> <data android:scheme="element" />
<data android:host="user" /> <data android:host="user" />
<data android:host="room" /> <data android:host="room" />
</intent-filter> </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 --> <!-- Incoming share simple -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@@ -9,19 +9,14 @@
package io.element.android.x package io.element.android.x
import android.app.Application import android.app.Application
import android.os.StrictMode
import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled import androidx.compose.material3.ComposeMaterial3Flags.isAnchoredDraggableComponentsStrictOffsetCheckEnabled
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.startup.AppInitializer import androidx.startup.AppInitializer
import androidx.work.Configuration import androidx.work.Configuration
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.createGraphFactory import dev.zacsweers.metro.createGraphFactory
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.di.DependencyInjectionGraphOwner 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.libraries.workmanager.api.di.MetroWorkerFactory
import io.element.android.x.di.AppGraph 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.info.logApplicationInfo
import io.element.android.x.initializer.CacheCleanerInitializer import io.element.android.x.initializer.CacheCleanerInitializer
import io.element.android.x.initializer.CrashInitializer import io.element.android.x.initializer.CrashInitializer
@@ -34,37 +29,19 @@ class ElementXApplication : Application(), DependencyInjectionGraphOwner, Config
.setWorkerFactory(MetroWorkerFactory(graph.workerProviders)) .setWorkerFactory(MetroWorkerFactory(graph.workerProviders))
.build() .build()
@Inject lateinit var sdkMetadata: SdkMetadata
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
enableStrictMode()
AppInitializer.getInstance(this).apply { AppInitializer.getInstance(this).apply {
initializeComponent(CrashInitializer::class.java) initializeComponent(CrashInitializer::class.java)
initializeComponent(PlatformInitializer::class.java) initializeComponent(PlatformInitializer::class.java)
initializeComponent(CacheCleanerInitializer::class.java) initializeComponent(CacheCleanerInitializer::class.java)
} }
bindings<ApplicationBindings>().inject(this) logApplicationInfo(this)
logApplicationInfo(this, sdkMetadata.sdkGitSha)
// Disable the strict offset check for anchored draggable components, as it can cause issues with bottom sheets. // 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. // Remove once https://issuetracker.google.com/issues/477038695 is fixed.
isAnchoredDraggableComponentsStrictOffsetCheckEnabled = false 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 { setContent {
MainContent(appBindings) 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 @Composable
@@ -10,13 +10,10 @@ package io.element.android.x.di
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Multibinds import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.callnative.api.NativeCallPip
import io.element.android.features.enterprise.api.AppStartupHook
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenService 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.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -53,15 +50,5 @@ interface AppBindings {
fun buildMeta(): BuildMeta 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? 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.Provides
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.appconfig.ApplicationConfig 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.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta 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.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext 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.BuildConfig
import io.element.android.x.R import io.element.android.x.R
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineName
@@ -38,7 +40,7 @@ import java.io.File
@BindingContainer @BindingContainer
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
object AppBindingContainer { object AppModule {
@Provides @Provides
@BaseDirectory @BaseDirectory
fun providesBaseDirectory(@ApplicationContext context: Context): File { fun providesBaseDirectory(@ApplicationContext context: Context): File {
@@ -74,18 +76,17 @@ object AppBindingContainer {
fun providesBuildMeta( fun providesBuildMeta(
@ApplicationContext context: Context, @ApplicationContext context: Context,
buildType: BuildType, buildType: BuildType,
isEnterpriseBuild: IsEnterpriseBuild, enterpriseService: EnterpriseService,
): BuildMeta { ): BuildMeta {
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name) val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
val isEnterprise = isEnterpriseBuild()
return BuildMeta( return BuildMeta(
isDebuggable = BuildConfig.DEBUG, isDebuggable = BuildConfig.DEBUG,
buildType = buildType, buildType = buildType,
applicationName = applicationName, applicationName = applicationName,
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME, productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME, desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
applicationId = BuildConfig.APPLICATION_ID, applicationId = BuildConfig.APPLICATION_ID,
isEnterpriseBuild = isEnterprise, isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
lowPrivacyLoggingEnabled = false, lowPrivacyLoggingEnabled = false,
versionName = BuildConfig.VERSION_NAME, versionName = BuildConfig.VERSION_NAME,
@@ -111,7 +112,13 @@ object AppBindingContainer {
@Provides @Provides
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
fun providesSnackbarDispatcher(): SnackbarDispatcher { fun provideSnackbarDispatcher(): SnackbarDispatcher {
return 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) @ContributesBinding(AppScope::class)
class DefaultSessionGraphFactory( class DefaultSessionGraphFactory(
private val appGraph: AppGraph, private val appGraph: AppGraph
) : SessionGraphFactory { ) : SessionGraphFactory {
override fun create(client: MatrixClient): Any { override fun create(client: MatrixClient): Any {
return appGraph.sessionGraphFactory.create(client) return appGraph.sessionGraphFactory.create(client)
@@ -10,22 +10,32 @@ package io.element.android.x.info
import android.content.Context import android.content.Context
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
import io.element.android.libraries.core.log.logger.wrapInBox
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import timber.log.Timber import timber.log.Timber
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
fun logApplicationInfo(context: Context, sdkGitSha: String) { fun logApplicationInfo(context: Context) {
val appVersion = "${BuildConfig.VERSION_NAME} (${context.getVersionCodeFromManifest()}) - ${BuildConfig.BUILD_TYPE} / ${BuildConfig.FLAVOR}" 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()) val date = SimpleDateFormat("MM-dd HH:mm:ss.SSSZ", Locale.US).format(Date())
listOf(
" Application version: $appVersion", Timber.d("----------------------------------------------------------------")
" Element X: https://github.com/element-hq/element-x-android/commit/${BuildConfig.GIT_REVISION}", Timber.d("----------------------------------------------------------------")
" SDK : https://github.com/matrix-org/matrix-rust-sdk/commit/$sdkGitSha", Timber.d(" Application version: $appVersion")
" Local time: $date", Timber.d(" Git SHA: ${BuildConfig.GIT_REVISION}")
) Timber.d(" SDK version: $sdkVersion")
.wrapInBox(minBoxInsideWidth = 80) Timber.d(" Local time: $date")
.forEach { Timber.d(it) } Timber.d("----------------------------------------------------------------")
Timber.d("----------------------------------------------------------------\n\n\n\n")
} }
@@ -8,14 +8,11 @@
package io.element.android.x.initializer package io.element.android.x.initializer
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.system.Os import android.system.Os
import android.util.Log
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration import io.element.android.features.rageshake.api.logs.createWriteToFilesConfiguration
import io.element.android.libraries.architecture.bindings 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.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
@@ -27,15 +24,6 @@ private const val ELEMENT_X_TARGET = "elementx"
class PlatformInitializer : Initializer<Unit> { class PlatformInitializer : Initializer<Unit> {
override fun create(context: Context) { 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 appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService() val tracingService = appBindings.tracingService()
val platformService = appBindings.platformService() val platformService = appBindings.platformService()
@@ -53,8 +41,6 @@ class PlatformInitializer : Initializer<Unit> {
sdkSentryDsn = appBindings.sentrySdkDsn()?.value?.takeIf { it.isNotBlank() }, sdkSentryDsn = appBindings.sentrySdkDsn()?.value?.takeIf { it.isNotBlank() },
) )
bugReporter.setCurrentTracingLogLevel(logLevel.name) bugReporter.setCurrentTracingLogLevel(logLevel.name)
// Init the tracing service with the configuration so we have logging enabled as soon as possible
platformService.init(tracingConfiguration) platformService.init(tracingConfiguration)
// Also set env variable for rust back trace // Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true) Os.setenv("RUST_BACKTRACE", "1", true)
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_onboarding_welcome_title">"Willkommen bei HealthChat"</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_onboarding_welcome_title">"Bienvenue chez HealthChat"</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources>
<string name="screen_onboarding_welcome_title">"Benvenuto su HealthChat"</string>
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2026 Novaloop AG
~
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
~ Please see LICENSE files in the repository root for full details.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- HealthChat override of the Element onboarding title ("Be in your element").
Overridden here app-side instead of editing the Localazy-managed login
strings, so upstream merges stay clean. Only the main HealthChat locales
are overridden; other locales fall back to their Element translation. -->
<string name="screen_onboarding_welcome_title" tools:ignore="MissingTranslation">"Welcome to HealthChat"</string>
</resources>
-1
View File
@@ -1,6 +1,5 @@
<!-- File generated by importSupportedLocalesFromLocalazy.py, do not edit --> <!-- File generated by importSupportedLocalesFromLocalazy.py, do not edit -->
<locale-config xmlns:android="http://schemas.android.com/apk/res/android"> <locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="ar"/>
<locale android:name="be"/> <locale android:name="be"/>
<locale android:name="bg"/> <locale android:name="bg"/>
<locale android:name="ca"/> <locale android:name="ca"/>
@@ -30,384 +30,5 @@
<domain includeSubdomains="true">home</domain> <domain includeSubdomains="true">home</domain>
<domain includeSubdomains="true">lan</domain> <domain includeSubdomains="true">lan</domain>
<domain includeSubdomains="true">localdomain</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> </domain-config>
</network-security-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 { defaultConfig {
buildConfigFieldStr( buildConfigFieldStr(
name = "URL_POLICY", name = "URL_POLICY",
// HealthChat fork: a configured value wins for every build type, not only enterprise ones. value = if (isEnterpriseBuild) {
value = BuildTimeConfig.URL_POLICY ?: if (isEnterpriseBuild) "" else "https://element.io/cookie-policy", BuildTimeConfig.URL_POLICY ?: ""
} else {
"https://element.io/cookie-policy"
},
) )
buildConfigFieldStr( buildConfigFieldStr(
name = "BUG_REPORT_URL", name = "BUG_REPORT_URL",
// HealthChat fork: a configured value wins for every build type, not only enterprise ones. value = if (isEnterpriseBuild) {
value = BuildTimeConfig.BUG_REPORT_URL ?: if (isEnterpriseBuild) "" else "https://rageshakes.element.io/api/submit", BuildTimeConfig.BUG_REPORT_URL ?: ""
} else {
"https://rageshakes.element.io/api/submit"
},
) )
buildConfigFieldStr( buildConfigFieldStr(
name = "BUG_REPORT_APP_NAME", name = "BUG_REPORT_APP_NAME",
@@ -42,6 +48,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.coroutines.core)
implementation(libs.androidx.annotationjvm) implementation(libs.androidx.annotationjvm)
implementation(libs.androidx.corektx) implementation(libs.androidx.corektx)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
@@ -9,8 +9,15 @@
package io.element.android.appconfig package io.element.android.appconfig
object AuthenticationConfig { 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. * 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" 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 package io.element.android.appconfig
object LearnMoreConfig { object LearnMoreConfig {
// HealthChat fork: every "learn more" link points at the HealthChat site instead of element.io/help. const val ENCRYPTION_URL: String = "https://element.io/help#encryption"
const val ENCRYPTION_URL: String = "https://www.healthchat.ch" const val DEVICE_VERIFICATION_URL: String = "https://element.io/help#encryption-device-verification"
const val DEVICE_VERIFICATION_URL: String = "https://www.healthchat.ch" const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val SECURE_BACKUP_URL: String = "https://www.healthchat.ch" const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
const val IDENTITY_CHANGE_URL: String = "https://www.healthchat.ch" const val HISTORY_VISIBLE_URL: String = "https://element.io/en/help#e2ee-history-sharing"
const val HISTORY_VISIBLE_URL: String = "https://www.healthchat.ch"
} }
@@ -10,19 +10,5 @@ package io.element.android.appconfig
object MatrixConfiguration { object MatrixConfiguration {
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/" const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
/**
* HealthChat's own matrix.to deployment (https://github.com/Novaloop-AG/healthchat-matrix.to).
* It uses the exact same URL format as matrix.to, so links can be translated by swapping the
* base URL. Used both to recognise incoming links and to build links shared outside of Matrix.
*/
const val HEALTHCHAT_TO_PERMALINK_BASE_URL: String = "https://to.healthchat.ch/#/"
/**
* Base URL of the HealthChat web client, which is what the healthchat:// custom scheme
* handed out by [HEALTHCHAT_TO_PERMALINK_BASE_URL] points to.
*/
const val HEALTHCHAT_WEB_BASE_URL: String = "https://app.healthchat.ch/#/"
val clientPermalinkBaseUrl: String? = null val clientPermalinkBaseUrl: String? = null
} }
@@ -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. * The maximum number of log lines a rageshake can contain.
*/ */
const val MAX_LOG_LINES_SIZE = 1_000_000 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
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

-10
View File
@@ -34,7 +34,6 @@ dependencies {
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.oauth.api) implementation(projects.libraries.oauth.api)
implementation(projects.libraries.permissions.api)
implementation(projects.libraries.preferences.api) implementation(projects.libraries.preferences.api)
implementation(projects.libraries.push.api) implementation(projects.libraries.push.api)
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
@@ -49,7 +48,6 @@ dependencies {
implementation(libs.coil) implementation(libs.coil)
implementation(projects.features.announcement.api) implementation(projects.features.announcement.api)
implementation(projects.features.callnative.api)
implementation(projects.features.ftue.api) implementation(projects.features.ftue.api)
implementation(projects.features.linknewdevice.api) implementation(projects.features.linknewdevice.api)
implementation(projects.features.share.api) implementation(projects.features.share.api)
@@ -61,23 +59,15 @@ dependencies {
testCommonDependencies(libs) testCommonDependencies(libs)
testImplementation(projects.features.login.test) testImplementation(projects.features.login.test)
testImplementation(projects.features.share.test) testImplementation(projects.features.share.test)
testImplementation(projects.libraries.accountselect.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.matrixmedia.test)
testImplementation(projects.libraries.oauth.test) testImplementation(projects.libraries.oauth.test)
testImplementation(projects.libraries.permissions.test)
testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.sessionStorage.test)
testImplementation(projects.features.announcement.test)
testImplementation(projects.features.forward.test) testImplementation(projects.features.forward.test)
testImplementation(projects.features.messages.test) testImplementation(projects.features.messages.test)
testImplementation(projects.features.networkmonitor.test) testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.features.preferences.test)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.signedout.test)
testImplementation(projects.services.apperror.test) testImplementation(projects.services.apperror.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.analytics.test) testImplementation(projects.services.analytics.test)
@@ -8,15 +8,9 @@
package io.element.android.appnav 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.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.NewRoot
import com.bumble.appyx.navmodel.backstack.operation.Remove 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. * 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 } ?: return
accept(Remove(lastExpectedNavElement.key)) 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.architecture.inputs
import io.element.android.libraries.di.DependencyInjectionGraphOwner import io.element.android.libraries.di.DependencyInjectionGraphOwner
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@@ -60,7 +59,6 @@ class LoggedInAppScopeFlowNode(
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToBugReport() fun navigateToBugReport()
fun navigateToAddAccount() fun navigateToAddAccount()
fun navigateToSession(sessionId: SessionId)
} }
private val callback: Callback = callback() private val callback: Callback = callback()
@@ -93,10 +91,6 @@ class LoggedInAppScopeFlowNode(
override fun navigateToAddAccount() { override fun navigateToAddAccount() {
callback.navigateToAddAccount() callback.navigateToAddAccount()
} }
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
} }
return createNode<LoggedInFlowNode>(buildContext, listOf(callback)) return createNode<LoggedInFlowNode>(buildContext, listOf(callback))
} }
@@ -8,51 +8,28 @@
package io.element.android.appnav package io.element.android.appnav
import androidx.annotation.VisibleForTesting import dev.zacsweers.metro.Inject
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 io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage 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.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.matrix.api.timeline.item.event.MembershipChange
import io.element.android.libraries.push.api.notifications.NotificationCleaner
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@AssistedInject @Inject
class LoggedInEventProcessor( class LoggedInEventProcessor(
@Assisted private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val roomMembershipObserver: RoomMembershipObserver, private val roomMembershipObserver: RoomMembershipObserver,
private val sessionId: SessionId,
private val syncService: SyncService,
private val notificationCleaner: NotificationCleaner,
private val dispatchers: CoroutineDispatchers
) { ) {
@AssistedFactory private var observingJob: Job? = null
interface Factory {
fun create(snackbarDispatcher: SnackbarDispatcher): LoggedInEventProcessor
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var currentChildScope: CoroutineScope? = null
fun observeEvents(coroutineScope: CoroutineScope) { fun observeEvents(coroutineScope: CoroutineScope) {
if (currentChildScope != null) return observingJob = roomMembershipObserver.updates
val childScope = coroutineScope.childScope(dispatchers.computation, "LoggedInEventProcessor")
.also { currentChildScope = it }
roomMembershipObserver.updates
.filter { !it.isUserInRoom } .filter { !it.isUserInRoom }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { roomMemberShipUpdate -> .onEach { roomMemberShipUpdate ->
@@ -71,19 +48,12 @@ class LoggedInEventProcessor(
else -> Unit else -> Unit
} }
} }
.launchIn(childScope) .launchIn(coroutineScope)
// Use the sync service heartbeat as a 'heartbeat' update to check existing notifications
syncService.heartBeat
.onEach {
notificationCleaner.clearReadRoomsNotifications(sessionId)
}
.launchIn(childScope)
} }
fun stopObserving() { fun stopObserving() {
currentChildScope?.cancel() observingJob?.cancel()
currentChildScope = null observingJob = null
} }
private fun displayMessage(message: Int) { private fun displayMessage(message: Int) {
@@ -11,7 +11,6 @@ package io.element.android.appnav
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.compound.colors.SemanticColorsLightDark 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.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService 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.ftue.api.state.FtueState
import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.home.api.HomeEntryPoint
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint 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.location.api.live.ActiveLiveLocationShareManager
import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus 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.RoomDescription
import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint import io.element.android.features.roomdirectory.api.RoomDirectoryEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint 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.startchat.api.StartChatEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint 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.EventId
import io.element.android.libraries.matrix.api.core.RoomId 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.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.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias 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.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.JoinedRoom 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.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.matrix.api.verification.VerificationRequest
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService 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.libraries.ui.common.nodes.emptyNode
import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction import io.element.android.services.analytics.api.AnalyticsLongRunningTransaction
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@@ -108,10 +105,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import java.time.Duration
import java.time.Instant
import java.util.Optional import java.util.Optional
import java.util.UUID 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.Duration.Companion.seconds
import kotlin.time.toKotlinDuration
import im.vector.app.features.analytics.plan.JoinedRoom as JoinedRoomAnalyticsEvent 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. // 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 sessionCoroutineScope: CoroutineScope,
private val ftueService: FtueService, private val ftueService: FtueService,
private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint, private val roomDirectoryEntryPoint: RoomDirectoryEntryPoint,
private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues, private val sendingQueue: SendQueues,
private val nativeCallHost: NativeCallHost,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService, private val sessionEnterpriseService: SessionEnterpriseService,
@@ -153,8 +153,6 @@ class LoggedInFlowNode(
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher, private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
private val createRoomEntryPoint: CreateRoomEntryPoint, private val createRoomEntryPoint: CreateRoomEntryPoint,
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager, private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
private val locationService: LocationService,
private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>( ) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Placeholder, initialElement = NavTarget.Placeholder,
@@ -170,26 +168,44 @@ class LoggedInFlowNode(
interface Callback : Plugin { interface Callback : Plugin {
fun navigateToBugReport() fun navigateToBugReport()
fun navigateToAddAccount() fun navigateToAddAccount()
fun navigateToSession(sessionId: SessionId)
} }
private val callback: Callback = callback() private val callback: Callback = callback()
private val loggedInFlowProcessor = loggedInEventProcessorFactory.create(snackbarDispatcher) private val loggedInFlowProcessor = LoggedInEventProcessor(
snackbarDispatcher = snackbarDispatcher,
roomMembershipObserver = matrixClient.roomMembershipObserver,
)
/** private val verificationListener = object : SessionVerificationServiceListener {
* Display the incoming verification request, the app is expected to be in foreground. 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.
fun onIncomingVerificationRequest(verificationRequest: VerificationRequest.Incoming) { // This might be because this method is called back from Rust in a background thread.
lifecycleScope.launch { lifecycleScope.launch {
// Wait for the RoomList UI to be ready so the incoming verification screen can be displayed on top of it val receivedAt = Instant.now()
// Otherwise, the RoomList UI may be incorrectly displayed on top
withTimeout(5.seconds) { // Wait until the app is in foreground to display the incoming verification request
backstack.elements.first { elements -> appNavigationStateService.appNavigationState.first { it.isInForeground }
elements.any { it.key.navTarget == NavTarget.Home }
// 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() analyticsRoomListStateWatcher.start()
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId) appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
loggedInFlowProcessor.observeEvents(sessionCoroutineScope) loggedInFlowProcessor.observeEvents(sessionCoroutineScope)
matrixClient.sessionVerificationService.setListener(verificationListener)
mediaPreviewConfigMigration() mediaPreviewConfigMigration()
sessionCoroutineScope.launch { sessionCoroutineScope.launch {
// Wait for the network to be connected before pre-fetching the max file upload size // Wait for the network to be connected before pre-fetching the max file upload size
@@ -232,6 +249,7 @@ class LoggedInFlowNode(
onDestroy = { onDestroy = {
appNavigationStateService.onLeavingSession(id) appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving() loggedInFlowProcessor.stopObserving()
matrixClient.sessionVerificationService.setListener(null)
analyticsRoomListStateWatcher.stop() analyticsRoomListStateWatcher.stop()
} }
) )
@@ -292,15 +310,16 @@ class LoggedInFlowNode(
@Parcelize @Parcelize
data object RoomDirectory : NavTarget data object RoomDirectory : NavTarget
@Parcelize
data class IncomingShare(val shareIntentData: ShareIntentData) : NavTarget
@Parcelize @Parcelize
data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget data class IncomingVerificationRequest(val data: VerificationRequest.Incoming) : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
return when (navTarget) { return when (navTarget) {
// Rendered while the FtueState is Unknown. Render a loading state rather than nothing at all, NavTarget.Placeholder -> emptyNode(buildContext)
// since there is no guarantee that this state will be left.
NavTarget.Placeholder -> emptyNode(buildContext, state = EmptyState(delayBeforeShowingContent = 500.milliseconds))
NavTarget.LoggedInPermanent -> { NavTarget.LoggedInPermanent -> {
val callback = object : LoggedInNode.Callback { val callback = object : LoggedInNode.Callback {
override fun navigateToNotificationTroubleshoot() { override fun navigateToNotificationTroubleshoot() {
@@ -311,11 +330,11 @@ class LoggedInFlowNode(
} }
NavTarget.Home -> { NavTarget.Home -> {
val callback = object : HomeEntryPoint.Callback { val callback = object : HomeEntryPoint.Callback {
override fun navigateToRoom(roomId: RoomId, eventId: EventId?, joinedRoom: JoinedRoom?) { override fun navigateToRoom(roomId: RoomId, joinedRoom: JoinedRoom?) {
lifecycleScope.launch { lifecycleScope.launch {
attachRoom( attachRoom(
roomIdOrAlias = roomId.toRoomIdOrAlias(), roomIdOrAlias = roomId.toRoomIdOrAlias(),
initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom, eventId = eventId), initialElement = RoomNavigationTarget.Root(joinedRoom = joinedRoom),
clearBackstack = false, clearBackstack = false,
) )
} }
@@ -354,19 +373,6 @@ class LoggedInFlowNode(
override fun navigateToBugReport() { override fun navigateToBugReport() {
callback.navigateToBugReport() 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))
}
} }
homeEntryPoint.createNode( homeEntryPoint.createNode(
parentNode = this, parentNode = this,
@@ -459,10 +465,6 @@ class LoggedInFlowNode(
callback.navigateToAddAccount() callback.navigateToAddAccount()
} }
override fun navigateToSession(sessionId: SessionId) {
callback.navigateToSession(sessionId)
}
override fun navigateToLinkNewDevice() { override fun navigateToLinkNewDevice() {
backstack.push(NavTarget.LinkNewDevice) backstack.push(NavTarget.LinkNewDevice)
} }
@@ -576,6 +578,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 -> { is NavTarget.IncomingVerificationRequest -> {
incomingVerificationEntryPoint.createNode( incomingVerificationEntryPoint.createNode(
parentNode = this, parentNode = this,
@@ -591,15 +617,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( suspend fun attachRoom(
roomIdOrAlias: RoomIdOrAlias, roomIdOrAlias: RoomIdOrAlias,
serverNames: List<String> = emptyList(), serverNames: List<String> = emptyList(),
@@ -644,14 +661,22 @@ class LoggedInFlowNode(
} }
} }
internal suspend fun attachIncomingShare(shareIntentData: ShareIntentData) {
waitForNavTargetAttached { navTarget ->
navTarget is NavTarget.Home
}
attachChild<Node> {
backstack.push(
NavTarget.IncomingShare(shareIntentData)
)
}
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val colors by remember { val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId) enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
}.collectAsState(SemanticColorsLightDark.default) }.collectAsState(SemanticColorsLightDark.default)
val mapTilerConfig by locationService.mapTilerConfig.collectAsState()
ElementThemeApp( ElementThemeApp(
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
@@ -659,22 +684,16 @@ class LoggedInFlowNode(
compoundDark = colors.dark, compoundDark = colors.dark,
buildMeta = buildMeta, buildMeta = buildMeta,
) { ) {
CompositionLocalProvider(LocalMapTilerConfig provides mapTilerConfig) { val isOnline by syncService.isOnline.collectAsState()
val isOnline by syncService.isOnline.collectAsState() ConnectivityIndicatorContainer(
// Outside the connectivity indicator so the two strips stack rather than fight: an isOnline = isOnline,
// offline banner during a call belongs under the call bar, not over it. modifier = modifier,
nativeCallHost.Render(modifier = modifier) { callContentModifier -> ) { contentModifier ->
ConnectivityIndicatorContainer( Box(modifier = contentModifier) {
isOnline = isOnline, val ftueState by ftueService.state.collectAsState()
modifier = callContentModifier, BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
) { contentModifier -> if (ftueState is FtueState.Complete) {
Box(modifier = contentModifier) { PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
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 android.os.Parcelable
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import coil3.SingletonImageLoader import coil3.SingletonImageLoader
import coil3.annotation.DelicateCoilApi import coil3.annotation.DelicateCoilApi
import com.bumble.appyx.core.lifecycle.subscribe 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.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode 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.LoginEntryPoint
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.libraries.architecture.BackstackView 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.architecture.inputs
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
import io.element.android.libraries.designsystem.utils.ScreenOrientation import io.element.android.libraries.designsystem.utils.ScreenOrientation
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrixmedia.api.ImageLoaderHolder
import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher import io.element.android.services.analytics.api.watchers.AnalyticsColdStartWatcher
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@@ -51,8 +45,6 @@ class NotLoggedInFlowNode(
private val loginEntryPoint: LoginEntryPoint, private val loginEntryPoint: LoginEntryPoint,
private val imageLoaderHolder: ImageLoaderHolder, private val imageLoaderHolder: ImageLoaderHolder,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher, private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
private val featureFlagService: FeatureFlagService,
private val announcementService: AnnouncementService,
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>( ) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
@@ -76,13 +68,6 @@ class NotLoggedInFlowNode(
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
analyticsColdStartWatcher.whenLoggingIn() 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( lifecycle.subscribe(
onResume = { onResume = {
SingletonImageLoader.setUnsafe(imageLoaderHolder.get()) SingletonImageLoader.setUnsafe(imageLoaderHolder.get())
@@ -32,6 +32,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.annotations.ContributesNode 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.IntentResolver
import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.intent.ResolvedIntent
import io.element.android.appnav.room.RoomFlowNode 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.RootNavStateFlowFactory
import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootPresenter
import io.element.android.appnav.root.RootView 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.announcement.api.AnnouncementService
import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint 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.share.api.ShareIntentData
import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint 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.appyx.rememberDelegateTransitionHandler
import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForChildAttached 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.deeplink.api.DeeplinkData
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags 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.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.SessionId
import io.element.android.libraries.matrix.api.core.ThreadId 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.asEventId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias 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.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.OAuthAction
import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.oauth.api.OAuthActionFlow
import io.element.android.libraries.sessionstorage.api.LoggedInState 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.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
@AssistedInject @AssistedInject
@@ -113,8 +100,6 @@ class RootFlowNode(
private val announcementService: AnnouncementService, private val announcementService: AnnouncementService,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val analyticsColdStartWatcher: AnalyticsColdStartWatcher, private val analyticsColdStartWatcher: AnalyticsColdStartWatcher,
private val incomingVerificationRequestObserver: IncomingVerificationRequestObserver,
private val shareEntryPoint: ShareEntryPoint,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope, @AppCoroutineScope private val appCoroutineScope: CoroutineScope,
) : BaseFlowNode<RootFlowNode.NavTarget>( ) : BaseFlowNode<RootFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
@@ -124,13 +109,6 @@ class RootFlowNode(
buildContext = buildContext, buildContext = buildContext,
plugins = plugins 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() { override fun onBuilt() {
analyticsColdStartWatcher.start() analyticsColdStartWatcher.start()
appCoroutineScope.launch { appCoroutineScope.launch {
@@ -142,8 +120,6 @@ class RootFlowNode(
observeNavState(false) observeNavState(false)
} }
} }
observeIncomingVerificationRequests()
observeRemovedSessions()
super.onBuilt() super.onBuilt()
} }
@@ -176,7 +152,7 @@ class RootFlowNode(
} }
} }
LoggedInState.NotLoggedIn -> { LoggedInState.NotLoggedIn -> {
switchToNotLoggedInFlow(pendingLoginParams) switchToNotLoggedInFlow(null)
} }
} }
} }
@@ -218,93 +194,16 @@ class RootFlowNode(
} }
private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) {
pendingLoginParams = null backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId))
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))
}
} }
private fun switchToNotLoggedInFlow(params: LoginParams?) { private fun switchToNotLoggedInFlow(params: LoginParams?) {
Timber.d("switchToNotLoggedInFlow, hasLoginParams=${params != null}")
matrixSessionCache.removeAll() matrixSessionCache.removeAll()
backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
} }
private fun switchToSignedOutFlow(sessionId: SessionId) { private fun switchToSignedOutFlow(sessionId: SessionId) {
pendingLoginParams = null backstack.safeRoot(NavTarget.SignedOutFlow(sessionId))
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)
}
} }
private suspend fun restoreSessionIfNeeded( private suspend fun restoreSessionIfNeeded(
@@ -368,7 +267,8 @@ class RootFlowNode(
@Parcelize data class AccountSelect( @Parcelize data class AccountSelect(
val currentSessionId: SessionId, val currentSessionId: SessionId,
val permalinkData: PermalinkData, val shareIntentData: ShareIntentData?,
val permalinkData: PermalinkData?,
) : NavTarget ) : NavTarget
@Parcelize data class NotLoggedInFlow( @Parcelize data class NotLoggedInFlow(
@@ -384,20 +284,6 @@ class RootFlowNode(
) : NavTarget ) : NavTarget
@Parcelize data object BugReport : 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 { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@@ -416,12 +302,6 @@ class RootFlowNode(
override fun navigateToAddAccount() { override fun navigateToAddAccount() {
backstack.push(NavTarget.NotLoggedInFlow(null)) backstack.push(NavTarget.NotLoggedInFlow(null))
} }
override fun navigateToSession(sessionId: SessionId) {
lifecycleScope.launch {
sessionStore.setLatestSession(sessionId.value)
}
}
} }
val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap) val savedNavState = extractSavedStateForNavTarget(navTarget, this.buildContext.savedStateMap)
val buildContext = if (savedNavState != null) { val buildContext = if (savedNavState != null) {
@@ -439,7 +319,6 @@ class RootFlowNode(
} }
override fun onDone() { override fun onDone() {
pendingLoginParams = null
backstack.pop() backstack.pop()
} }
} }
@@ -458,73 +337,6 @@ class RootFlowNode(
) )
} }
NavTarget.SplashScreen -> emptyNode(buildContext) 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 -> { NavTarget.BugReport -> {
val callback = object : BugReportEntryPoint.Callback { val callback = object : BugReportEntryPoint.Callback {
override fun onDone() { override fun onDone() {
@@ -546,7 +358,13 @@ class RootFlowNode(
// Do not pop when the account is changed to avoid a UI flicker. // Do not pop when the account is changed to avoid a UI flicker.
backstack.pop() 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) { private suspend fun onLoginLink(params: LoginParams) {
val accountProvider = AccountProvider.Generic(params.accountProvider) if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(params.accountProvider.ensureProtocol())) {
if (accountProviderAccessControl.isAllowedToConnectToAccountProvider(accountProvider)) {
// Is there a session already? // Is there a session already?
val sessions = sessionStore.getAllSessions() val sessions = sessionStore.getAllSessions()
if (sessions.isNotEmpty()) { if (sessions.isNotEmpty()) {
@@ -601,7 +418,6 @@ class RootFlowNode(
Timber.w("Login link ignored, multi account is disabled") Timber.w("Login link ignored, multi account is disabled")
} }
} else { } else {
pendingLoginParams = params
switchToNotLoggedInFlow(params) switchToNotLoggedInFlow(params)
} }
} else { } else {
@@ -616,12 +432,21 @@ class RootFlowNode(
// No session, open login // No session, open login
switchToNotLoggedInFlow(null) switchToNotLoggedInFlow(null)
} else { } else {
// Wait for the current session to be restored and for the FTUE to be complete, so that the session is verified, // wait for the current session to be restored
// then display the incoming share on top of it. val loggedInFlowNode = attachSession(latestSessionId)
// The incoming share does not belong to the current session: in case of multiple accounts, the user can switch account if (sessionStore.numberOfSessions() > 1) {
// from the room select screen, and the share is kept if the current session is signed out in the meantime. // Several accounts, let the user choose which one to use
attachSession(latestSessionId).waitForHome() backstack.push(
backstack.push(NavTarget.IncomingShare(latestSessionId, shareIntentData)) 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( backstack.push(
NavTarget.AccountSelect( NavTarget.AccountSelect(
currentSessionId = latestSessionId, currentSessionId = latestSessionId,
shareIntentData = null,
permalinkData = permalinkData, permalinkData = permalinkData,
) )
) )
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * 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 androidx.annotation.VisibleForTesting
import com.bumble.appyx.core.state.MutableSavedStateMap 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.libraries.matrix.api.core.SessionId
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analyticsproviders.api.AnalyticsUserData 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import timber.log.Timber import timber.log.Timber
@@ -44,12 +41,6 @@ class MatrixSessionCache(
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
) : MatrixClientProvider { ) : MatrixClientProvider {
private val sessionIdsToMatrixSession = ConcurrentHashMap<SessionId, InMemoryMatrixSession>() 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() private val restoreMutex = Mutex()
init { init {
@@ -60,12 +51,10 @@ class MatrixSessionCache(
fun removeAll() { fun removeAll() {
sessionIdsToMatrixSession.clear() sessionIdsToMatrixSession.clear()
updateMatrixClients()
} }
fun remove(sessionId: SessionId) { fun remove(sessionId: SessionId) {
sessionIdsToMatrixSession.remove(sessionId) sessionIdsToMatrixSession.remove(sessionId)
updateMatrixClients()
} }
override fun getOrNull(sessionId: SessionId): MatrixClient? { override fun getOrNull(sessionId: SessionId): MatrixClient? {
@@ -126,20 +115,15 @@ class MatrixSessionCache(
private fun onNewMatrixClient(matrixClient: MatrixClient) { private fun onNewMatrixClient(matrixClient: MatrixClient) {
val syncOrchestrator = syncOrchestratorFactory.create( val syncOrchestrator = syncOrchestratorFactory.create(
matrixClient = matrixClient, syncService = matrixClient.syncService,
sessionCoroutineScope = matrixClient.sessionCoroutineScope, sessionCoroutineScope = matrixClient.sessionCoroutineScope,
) )
sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession( sessionIdsToMatrixSession[matrixClient.sessionId] = InMemoryMatrixSession(
matrixClient = matrixClient, matrixClient = matrixClient,
syncOrchestrator = syncOrchestrator, syncOrchestrator = syncOrchestrator,
) )
updateMatrixClients()
syncOrchestrator.start() syncOrchestrator.start()
} }
private fun updateMatrixClients() {
_matrixClients.value = sessionIdsToMatrixSession.values.map { it.matrixClient }
}
} }
private data class InMemoryMatrixSession( private data class InMemoryMatrixSession(
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * 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 androidx.annotation.VisibleForTesting
import dev.zacsweers.metro.Assisted 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.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope 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.libraries.matrix.api.sync.SyncState
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.recordTransaction 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.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean 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.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@AssistedInject @AssistedInject
class SyncOrchestrator( class SyncOrchestrator(
@Assisted private val matrixClient: MatrixClient, @Assisted private val syncService: SyncService,
@Assisted sessionCoroutineScope: CoroutineScope, @Assisted sessionCoroutineScope: CoroutineScope,
private val appForegroundStateService: AppForegroundStateService, private val appForegroundStateService: AppForegroundStateService,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
@@ -51,25 +47,17 @@ class SyncOrchestrator(
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create( fun create(
matrixClient: MatrixClient, syncService: SyncService,
sessionCoroutineScope: CoroutineScope, sessionCoroutineScope: CoroutineScope,
): SyncOrchestrator ): SyncOrchestrator
} }
private val tag = "SyncOrchestrator" private val tag = "SyncOrchestrator"
private val syncService = matrixClient.syncService
private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag) private val coroutineScope = sessionCoroutineScope.childScope(dispatchers.io, tag)
private val started = AtomicBoolean(false) 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. * Starting observing the app state and network state to start/stop the sync service.
* *
@@ -101,23 +89,12 @@ class SyncOrchestrator(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun observeStates() = coroutineScope.launch { internal fun observeStates() = coroutineScope.launch {
Timber.tag(tag).d("start observing the app and network state") 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( val isAppActiveFlows = listOf(
appForegroundStateService.isInForeground, appForegroundStateService.isInForeground,
appForegroundStateService.isInCall, appForegroundStateService.isInCall,
appForegroundStateService.isSyncingNotificationEvent, appForegroundStateService.isSyncingNotificationEvent,
appForegroundStateService.hasRingingCall, appForegroundStateService.hasRingingCall,
appForegroundStateService.isSharingLiveLocation, appForegroundStateService.isSharingLiveLocation
) )
val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } } val isAppActiveFlow = combine(isAppActiveFlows) { actives -> actives.any { it } }
combine( combine(
@@ -126,11 +103,6 @@ class SyncOrchestrator(
networkMonitor.connectivity, networkMonitor.connectivity,
isAppActiveFlow, isAppActiveFlow,
) { syncState, networkState, isAppActive -> ) { 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 val isNetworkAvailable = networkState == NetworkStatus.Connected
Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable") Timber.tag(tag).d("isAppActive=$isAppActive, isNetworkAvailable=$isNetworkAvailable")
@@ -138,26 +110,14 @@ class SyncOrchestrator(
SyncStateAction.StopSync SyncStateAction.StopSync
} else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) { } else if (syncState == SyncState.Idle && isAppActive && isNetworkAvailable) {
SyncStateAction.StartSync 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 { } else {
SyncStateAction.NoOp SyncStateAction.NoOp
} }
} }
.distinctUntilChanged() .distinctUntilChanged()
.debounce { action -> .debounce { action ->
when (action) { // Don't stop the sync immediately, wait a bit to avoid starting/stopping the sync too often
// 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
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
}
} }
.onCompletion { .onCompletion {
Timber.tag(tag).d("has been stopped") Timber.tag(tag).d("has been stopped")
@@ -167,11 +127,6 @@ class SyncOrchestrator(
SyncStateAction.StartSync -> { SyncStateAction.StartSync -> {
syncService.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 -> { SyncStateAction.StopSync -> {
syncService.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 { private enum class SyncStateAction {
StartSync, StartSync,
RestartSync,
StopSync, StopSync,
NoOp, NoOp,
} }
@@ -76,7 +76,6 @@ class IntentResolver(
} }
private fun Intent.canBeIgnored(): Boolean { private fun Intent.canBeIgnored(): Boolean {
if (flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) return true
return action == Intent.ACTION_MAIN && return action == Intent.ACTION_MAIN &&
categories?.contains(Intent.CATEGORY_LAUNCHER) == true categories?.contains(Intent.CATEGORY_LAUNCHER) == true
} }
@@ -8,11 +8,8 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
sealed interface LoggedInEvent { sealed interface LoggedInEvents {
data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvent data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents
data object CheckSlidingSyncProxyAvailability : LoggedInEvent data object CheckSlidingSyncProxyAvailability : LoggedInEvents
data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvent data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents
data object DismissLocalNetworkPermissionPrompt : LoggedInEvent
data object RequestLocationNetworkPermission : LoggedInEvent
} }
@@ -8,7 +8,6 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import android.Manifest
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -16,7 +15,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus 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.PushService
import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.api.PusherRegistrationFailure
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@@ -64,12 +58,7 @@ class LoggedInPresenter(
private val encryptionService: EncryptionService, private val encryptionService: EncryptionService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val networkMonitor: NetworkMonitor, private val networkMonitor: NetworkMonitor,
private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) : Presenter<LoggedInState> { ) : Presenter<LoggedInState> {
private val localNetworkPermissionsPresenter: PermissionsPresenter =
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
@Composable @Composable
override fun present(): LoggedInState { override fun present(): LoggedInState {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -105,18 +94,9 @@ class LoggedInPresenter(
} }
val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState() val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState()
val isOnline by syncService.isOnline.collectAsState() val isOnline by syncService.isOnline.collectAsState()
val hasNetwork by networkMonitor.connectivity.collectAsState() val showSyncSpinner by remember {
val syncIndicatorState by remember {
derivedStateOf { derivedStateOf {
when { isOnline && syncIndicator == RoomListService.SyncIndicator.Show
isOnline -> if (syncIndicator == RoomListService.SyncIndicator.Show) {
SyncIndicatorState.Syncing
} else {
SyncIndicatorState.Hidden
}
hasNetwork == NetworkStatus.Connected -> SyncIndicatorState.ServerUnreachable
else -> SyncIndicatorState.Hidden
}
} }
} }
var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) } var forceNativeSlidingSyncMigration by remember { mutableStateOf(false) }
@@ -129,12 +109,6 @@ class LoggedInPresenter(
}.launchIn(this) }.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() val networkConnectivity by networkMonitor.connectivity.collectAsState()
LaunchedEffect(networkConnectivity) { LaunchedEffect(networkConnectivity) {
if (networkConnectivity == NetworkStatus.Connected) { if (networkConnectivity == NetworkStatus.Connected) {
@@ -143,24 +117,9 @@ class LoggedInPresenter(
} }
} }
val localNetworkPermissionState = localNetworkPermissionsPresenter.present() fun handleEvent(event: LoggedInEvents) {
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) {
when (event) { when (event) {
is LoggedInEvent.CloseErrorDialog -> { is LoggedInEvents.CloseErrorDialog -> {
pusherRegistrationState.value = AsyncData.Uninitialized pusherRegistrationState.value = AsyncData.Uninitialized
if (event.doNotShowAgain) { if (event.doNotShowAgain) {
coroutineScope.launch { coroutineScope.launch {
@@ -168,34 +127,22 @@ class LoggedInPresenter(
} }
} }
} }
LoggedInEvent.CheckSlidingSyncProxyAvailability -> coroutineScope.launch { LoggedInEvents.CheckSlidingSyncProxyAvailability -> coroutineScope.launch {
forceNativeSlidingSyncMigration = matrixClient.needsForcedNativeSlidingSyncMigration().getOrDefault(false) 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 // Force the logout since Native Sliding Sync is already enforced by the SDK
matrixClient.logout(userInitiated = true, ignoreSdkError = true) 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( return LoggedInState(
syncIndicatorState = syncIndicatorState, showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState.value, pusherRegistrationState = pusherRegistrationState.value,
ignoreRegistrationError = ignoreRegistrationError, ignoreRegistrationError = ignoreRegistrationError,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = buildMeta.applicationName, appName = buildMeta.applicationName,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }
@@ -9,20 +9,12 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import io.element.android.libraries.architecture.AsyncData 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( data class LoggedInState(
val syncIndicatorState: SyncIndicatorState, val showSyncSpinner: Boolean,
val pusherRegistrationState: AsyncData<Unit>, val pusherRegistrationState: AsyncData<Unit>,
val ignoreRegistrationError: Boolean, val ignoreRegistrationError: Boolean,
val forceNativeSlidingSyncMigration: Boolean, val forceNativeSlidingSyncMigration: Boolean,
val appName: String, val appName: String,
val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (LoggedInEvents) -> Unit,
val eventSink: (LoggedInEvent) -> Unit,
) )
@@ -10,34 +10,28 @@ package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.api.PusherRegistrationFailure
open class LoggedInStatePreviewParam : PreviewParameterProvider<LoggedInState> { open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState> override val values: Sequence<LoggedInState>
get() = sequenceOf( get() = sequenceOf(
aLoggedInState(), aLoggedInState(),
aLoggedInState(syncIndicatorState = SyncIndicatorState.Syncing), aLoggedInState(showSyncSpinner = true),
aLoggedInState(syncIndicatorState = SyncIndicatorState.ServerUnreachable),
aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())), aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())),
aLoggedInState(forceNativeSlidingSyncMigration = true), aLoggedInState(forceNativeSlidingSyncMigration = true),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale),
aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings),
) )
} }
fun aLoggedInState( fun aLoggedInState(
syncIndicatorState: SyncIndicatorState = SyncIndicatorState.Hidden, showSyncSpinner: Boolean = false,
pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized, pusherRegistrationState: AsyncData<Unit> = AsyncData.Uninitialized,
forceNativeSlidingSyncMigration: Boolean = false, forceNativeSlidingSyncMigration: Boolean = false,
appName: String = "Element X", appName: String = "Element X",
localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None,
) = LoggedInState( ) = LoggedInState(
syncIndicatorState = syncIndicatorState, showSyncSpinner = showSyncSpinner,
pusherRegistrationState = pusherRegistrationState, pusherRegistrationState = pusherRegistrationState,
ignoreRegistrationError = false, ignoreRegistrationError = false,
forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration,
appName = appName, appName = appName,
localNetworkPermissionDialog = localNetworkPermissionDialog,
eventSink = {}, eventSink = {},
) )

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