Compare commits

502 Commits

Author SHA1 Message Date
Sedat-Qaja 3b7ed0a48b fix: remove rlement endpoints and links from the HealthChat
* fix(healthchat): switch off analytics, crash and bug reporting endpoints

Upstream ships *.localhost placeholders in Secrets.swift and marks the file assume-unchanged; with analytics on, the Element Call widget was handed Element's own PostHog and Sentry. nil hides the analytics prompt, the Analytics and Report a problem entries until HealthChat runs its own services.

* feat(healthchat): hide the Labs entry in settings

* fix(healthchat): point Copyright and the analytics terms at healthchat.ch

* docs(healthchat): add the 26.11.1 release notes

* feat(healthchat): drop the Element help links

* feat(healthchat): switch off location sharing

* test(healthchat): skip the analytics suite without PostHog
2026-10-19 14:40:20 +02:00
Sedat Qaja 7b07b51a10 docs(healthchat): complete the 26.11.0 release notes 2026-10-19 10:09:19 +02:00
Sedat Qaja 8277cfb88c docs(healthchat): add the upstream sync skill
Patch-stack model, LFS handling, conflict hot spots, verification matrix, force-push and rewind, collected from the 26.09.1 → 26.10.3 sync.
2026-10-19 09:55:44 +02:00
Sedat Qaja 6f931434b5 docs(healthchat): update the runbook and add the 26.11.0 release notes 2026-10-19 09:18:09 +02:00
Sedat Qaja de5f6d9c75 test(healthchat): refresh snapshots after rebase onto 26.10.3 2026-10-19 09:18:09 +02:00
Sedat Qaja a80a6b8b0f fix(healthchat): keep legacy accounts on the typed account provider 2026-10-19 09:18:08 +02:00
Sedat Qaja f7d0a05763 fix(healthchat): rewire the NSE and call service to upstream's settings 2026-10-19 09:18:07 +02:00
Sedat Qaja 979d20572d refactor(healthchat): move the session hooks onto UserHooks 2026-10-19 09:18:07 +02:00
Sedat Qaja 1b34a71857 chore(healthchat): resolve the rebase onto upstream 26.10.3 2026-10-19 09:18:06 +02:00
Sedat Qaja 323970aa93 docs(healthchat): add the 26.10.1 release notes 2026-10-19 09:17:39 +02:00
Sedat Qaja 2d86c4b92c feat(healthchat): ring calls via gateway VoIP push
CallKit rejects the NSE hand-off without the filtering entitlement.
2026-10-19 09:17:38 +02:00
Sedat Qaja 71abd2097e chore: ignore the review working files 2026-10-19 09:17:37 +02:00
Sedat Qaja 5383515229 test(healthchat): record the snapshots for the approval screens 2026-10-19 09:17:37 +02:00
Sedat Qaja 1edfb8057f fix(healthchat): refresh the account status whenever settings appears
The banner kept telling the user to complete a profile they had just
completed, and nothing was fetched until the screen was actually shown.
2026-10-19 09:17:37 +02:00
Sedat Qaja d9524130fa feat(healthchat): advertise the account status in settings
Pending approval and an incomplete profile were only visible in the
members tab, which a new user has no reason to open.
2026-10-19 09:17:36 +02:00
Sedat Qaja 181c518dc7 feat(healthchat): show default contacts while approval is pending
An unapproved account only gets the server's default contacts, which are
its way to reach support — list them instead of discarding them, and hide
the search field that cannot return anything.
2026-10-19 09:17:35 +02:00
Sedat Qaja 57570df8dd Exclude derived data from SwiftFormat and move the release notes to the right file 2026-10-19 09:17:35 +02:00
Sedat Qaja 07dd2e2ef3 Add release notes for 26.09.1 2026-10-19 09:17:35 +02:00
Sedat Qaja 86369e858e Ship the NSE without the notification filtering entitlement
Apple has not granted com.apple.developer.usernotifications.filtering for
ch.healthchat.element.nse. The capability request has sat at "Submitted"
since 2026-07-30, so the App Store profile cannot carry it and the archive
fails to sign while NSE.entitlements still asks for it.

Drop the key from the NSE target config and the generated entitlements
file. The extension still decrypts and rewrites every notification; the
only change is that notifications it would have discarded now fall back
to the raw push payload. Restore the key and add NSE Filtering to the
profile once Apple approves the request.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-19 09:17:34 +02:00
joel.poyet 22a098ecff Build the fork with Xcode 27
Xcode 27 ships Swift 6.4, which rejects two things 26.09.1 still contains. The
upstream fix for the app itself is cherry-picked as the commit before this one;
these two are what it does not cover.

Compound's colour overrides now take a KeyPath & Sendable, so the HealthChat
brand arrays have to carry the same constraint.

NotificationContentBuilder is main-actor isolated, so its test suite has to
build the type from an async init. The lines match upstream's own fix, which
only exists inside the larger AccountProvider change (#6103) and so cannot be
cherry-picked on its own.
2026-10-19 09:17:34 +02:00
joel.poyet 7adc963cf9 Adapt the HealthChat hooks to the upstream 26.09.1 screen APIs
The rebase onto release/26.09.1 moved three things the fork depends on.

Upstream now owns a UserProfileScreenHook of its own (#5980), which rewrites a
screen's view state rather than injecting a view. The fork's section hook keeps
its behaviour under the fork-only name ExtendedProfileSectionHook, so both can
live side by side and upstream's file stays untouched. Upstream's new @AppHook
macro generates the same register<Name> methods, so registration is unchanged.

UserProfileScreenCoordinatorParameters lost appSettings and reordered its
arguments, and UserDetailsEditScreenCoordinator now publishes its own
UserDetailsEditScreenCoordinatorAction instead of borrowing the room one
(#5893). The members flow and the profile editor follow both.

AppSettings.override gained oAuthClientURIPath (#5934), whose default appends
apps/ios to the website URL. HealthChat passes nil to keep the registered
client_uri at https://healthchat.ch exactly as the shipped build has it —
changing what the app registers with MAS is not a side effect a rebase should
have.
2026-10-19 09:17:33 +02:00
Doug dcae1ffc6b Fix some isolation errors when compiling with Swift 6.4. (#6148)
* Fix some isolation errors when compiling with Swift 6.4.

* Update the Design Tokens dependency to include Sendable conformances.

(cherry picked from commit 4608a63e29)
2026-10-19 09:17:33 +02:00
joel.poyet 209a36cbea Let members edit their profile and invite others (#12)
* Add HealthChat API and extended profile services

Expose OpenID tokens and homeserver credentials from the client proxy, and
add fork-only services for the HealthChat member directory backend
(api.healthchat.ch) plus a service that reads MSC4133 extended profile
fields directly from the homeserver. Groundwork for the members tab and
the profile screens.

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

* Show extended profile details on the profile screens

Add a UserProfileScreenHook that lets the fork inject an extra Form
section below the header of the user profile and room member details
screens. The HealthChat implementation loads the MSC4133 extended
profile fields and renders them with tappable phone/email/website
links, hiding the section entirely for profiles without any fields.

Strings live in a new fork-only HealthChat.strings table (de/en/fr,
generated as HealthChatL10n via SwiftGen) since Untranslated.strings
pins the English bundle.

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

* Add a member directory tab

Add a HomeTabsHook that lets the fork append an extra tab to the home
tab bar, and a HealthChat members flow behind it: a directory of all
approved members from the HealthChat API with server-side search,
pending-approval and error states, chat shortcuts and pushed member
profiles. Chats and calls are handed back to the session flow via the
hook's actions publisher.

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

* Let members edit their profile and invite others

Add a native profile editor for the HealthChat extended profile fields
(13 fields plus avatar, saved as a partial diff through the HealthChat
API so displayname derivation and Zitadel sync stay server-side), an
invitation sheet (email plus personal note) and a profile-completeness
hint on the member directory.

A new UserDetailsEditScreenHook lets the fork substitute the profile
editor behind the settings user row — the stock display-name editor
would let users edit a value the next profile update derives away.

Claude-Session: https://claude.ai/code/session_013qdTbAWsZj2quQzEkagXaS
2026-10-19 09:17:32 +02:00
joel.poyet c0f167ccf7 Add a member directory tab (#11)
* Add HealthChat API and extended profile services

Expose OpenID tokens and homeserver credentials from the client proxy, and
add fork-only services for the HealthChat member directory backend
(api.healthchat.ch) plus a service that reads MSC4133 extended profile
fields directly from the homeserver. Groundwork for the members tab and
the profile screens.

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

* Show extended profile details on the profile screens

Add a UserProfileScreenHook that lets the fork inject an extra Form
section below the header of the user profile and room member details
screens. The HealthChat implementation loads the MSC4133 extended
profile fields and renders them with tappable phone/email/website
links, hiding the section entirely for profiles without any fields.

Strings live in a new fork-only HealthChat.strings table (de/en/fr,
generated as HealthChatL10n via SwiftGen) since Untranslated.strings
pins the English bundle.

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

* Add a member directory tab

Add a HomeTabsHook that lets the fork append an extra tab to the home
tab bar, and a HealthChat members flow behind it: a directory of all
approved members from the HealthChat API with server-side search,
pending-approval and error states, chat shortcuts and pushed member
profiles. Chats and calls are handed back to the session flow via the
hook's actions publisher.

Claude-Session: https://claude.ai/code/session_013qdTbAWsZj2quQzEkagXaS
2026-10-19 09:17:31 +02:00
joel.poyet dffeb4f9bb Show extended profile details on the profile screens (#10)
* Add HealthChat API and extended profile services

Expose OpenID tokens and homeserver credentials from the client proxy, and
add fork-only services for the HealthChat member directory backend
(api.healthchat.ch) plus a service that reads MSC4133 extended profile
fields directly from the homeserver. Groundwork for the members tab and
the profile screens.

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

* Show extended profile details on the profile screens

Add a UserProfileScreenHook that lets the fork inject an extra Form
section below the header of the user profile and room member details
screens. The HealthChat implementation loads the MSC4133 extended
profile fields and renders them with tappable phone/email/website
links, hiding the section entirely for profiles without any fields.

Strings live in a new fork-only HealthChat.strings table (de/en/fr,
generated as HealthChatL10n via SwiftGen) since Untranslated.strings
pins the English bundle.

Claude-Session: https://claude.ai/code/session_013qdTbAWsZj2quQzEkagXaS
2026-10-19 09:17:31 +02:00
joel.poyet e5bfc3a26d Add HealthChat API and extended profile services (#9)
Expose OpenID tokens and homeserver credentials from the client proxy, and
add fork-only services for the HealthChat member directory backend
(api.healthchat.ch) plus a service that reads MSC4133 extended profile
fields directly from the homeserver. Groundwork for the members tab and
the profile screens.

Claude-Session: https://claude.ai/code/session_013qdTbAWsZj2quQzEkagXaS
2026-10-19 09:17:29 +02:00
joel.poyet ea07f76c60 Add a skill for releasing HealthChat to TestFlight (#3)
Documents the release process this fork actually uses: tagging, the numeric-only
version rule, the five required secrets, the runner's hand-installed toolchain,
certificate renewal, and promoting a TestFlight build to the App Store. Modelled on
the legacy element-ios skill, corrected wherever this fork differs — notably that
there is no Xcode pin and no Brewfile, and that only five secrets exist rather than
the six the legacy skill lists.

The section with no upstream equivalent is the migration contract. CLASSIC_APP_*
and the .requiresBackup fall-through are load-bearing for every existing user and
fail silently, so a well-meaning cleanup back to upstream's values would sign
everyone out with nothing in CI to catch it.

AGENTS.md delegates to the skill rather than duplicating the runbook.
2026-10-19 09:17:29 +02:00
joel.poyet 8e81ebbd77 Add a fastlane pipeline that releases HealthChat to TestFlight (#2)
element-x-ios had no release machinery at all: no fastlane, no Gemfile, and none of
its workflows built or uploaded anything. Upstream ships via Xcode Cloud, which
needs Element's private Enterprise submodule and so is unavailable here, while the
legacy element-ios fork already proves a fastlane pipeline against the same Apple
team, certificate and App Store Connect key.

Tagging healthchat-v<version> now builds a signed App Store IPA on the org's Mac
Studio and uploads it to TestFlight. Release notes come from a new
HEALTHCHAT_CHANGES.md, because upstream's own tooling owns CHANGES.md.

The lane deliberately does not pin Xcode. The runner carries a single globally
selected version, and naming an absent one does not fail fast — it sends xcodes
into a multi-GB authenticated download that no CI secret can satisfy. Each run logs
xcodebuild -version instead, which is the only record of what compiled a build.

Everything here is fork-owned, since upstream has no fastlane directory, so none of
it can conflict on rebase.
2026-10-19 09:17:29 +02:00
joel.poyet 1da4c0302e Point the classic app identifiers at HealthChat rather than Element (#1)
ClassicAppManager migrates an existing user's session and encryption keys by
reading the legacy app's app-group container and keychain. Those identifiers
defaulted to Element's own, which this team cannot provision, so loadAccounts()
threw invalidAppGroupIdentifier — caught and logged, never surfaced. Since both
apps share a bundle id, the result would be every existing user silently logged
out and losing local history when they update.

The legacy identifiers belong to HealthChat and are provisionable under the same
team, so repoint them rather than dropping them.

Also stop sending accounts without key backup to a dead end. Upstream shows those
users a sheet asking them to open Element Classic and set up backup first, but this
update replaces that app and nothing registers its URL scheme, so the sheet's only
button does nothing. Sign in directly instead: the user re-verifies the device and
loses only history that by definition has no backup.
2026-10-19 09:17:29 +02:00
joel.poyet 9969af6984 Remove the internal superpowers planning docs (#5)
These two files are internal AI planning and design artifacts from setting up the
HealthChat release pipeline, not documentation meant to ship in the repo. Nothing
references them, and docs/FORKING.md remains the actual fork documentation.

Claude-Session: https://claude.ai/code/session_01PeF4yE5SDZV3zjXWk73osw
2026-10-19 09:17:28 +02:00
joel.poyet c352843fa6 Skip VoiceMessageCacheTests on the self-hosted runner (#8)
VoiceMessageCacheTests cannot write its URLFileProtection.complete cache file on this
runner's simulator, failing with NSCocoaErrorDomain 513. It is a runner-environment
issue, not a code bug: the suite passes locally and upstream, and neither recreating +
booting the device nor clearing state fixes it (diagnosed via the failing runs).

Add a small CI_SKIP_TESTS hook to the run-tests tool that forwards comma-separated test
identifiers to xcodebuild's -skip-testing, and set it to VoiceMessageCacheTests in
unit-tests.yml. This skips the suite on the runner only; it still runs everywhere the
env var is unset. Drop the CI_SKIP_TESTS line once the runner can write data-protected
files again.

Claude-Session: https://claude.ai/code/session_01PeF4yE5SDZV3zjXWk73osw
2026-10-19 09:17:28 +02:00
joel.poyet f5fdd9331d Make CI work on this internal fork (#4)
* Make CI work on this internal fork

Every check was red on every pull request. Upstream declares permissions: {} in its
workflows, which costs nothing on a public repo and is fatal here: the token is
limited to Metadata:read, so actions/checkout cannot read an internal repo and dies
with a misleading "Repository not found". Repo settings cannot fix it — an explicit
permissions block always overrides the default.

Each job now gets the minimum it needs. Danger also reads the pull request through
the issues API, so it needs issues:read alongside pull-requests:write, and zizmor's
SARIF upload needs actions:read on a private repo.

The unit tests move to the org's Mac Studio: GitHub-hosted macos-26 could not finish
inside the hour and bills at 10x. Preview tests are skipped there, using the flag
upstream already uses for its own enterprise tests — they render a snapshot per
preview and never once finished on either runner. The trade is that preview snapshot
regressions are no longer caught in CI.

Workflows that only make sense upstream are gated off rather than deleted, so they
keep rebasing cleanly. The enterprise tests need Element's bot token and their
private submodule. Three nightlies fail at checkout every weeknight — this repo is a
detached copy rather than a network fork, so GitHub never disabled their schedules —
and one of them posts to Element's Codecov on every run.

* Erase the test simulators before running the tests

The unit tests assume a clean simulator, which costs nothing on a hosted VM
because it hands out a fresh device every run. This runner reuses the same two
devices, so app containers, caches and logs survive between runs and six suites
fail on the previous run's leftovers rather than on anything in the code.
LoggingTests is the clearest: it expects no log files and finds nine, in the same
device container each time.

Only the two devices the tooling pins are erased, resolved by UDID. A blanket
erase would take out simulator state belonging to other work on a machine the
release pipeline shares.

* Remove the simulator reset again

It was added on the theory that the runner's reused simulators were leaking state
between runs. That theory was wrong: erasing both devices changed nothing — the
same seven suites failed identically before and after.

The leftover files LoggingTests trips on are its own, from a previous attempt of
the same test within one run (the suite runs with -test-iterations 4), so nothing
crosses runs for an erase to clean. Keeping a step that mutates shared state on the
release machine for no measured benefit is worse than not having it.
2026-10-19 09:17:28 +02:00
joel.poyet f5a98c393d Record the Xcode provenance mitigation as taken
O8 still recommended adding xcodebuild -version to the release workflow as
future work. The Record toolchain step now does exactly that, so the risk row
described a gap that no longer exists and advised work already done.

The residual risk is narrower than O8 claimed: each run logs its own compiler,
but the log only reveals a runner upgrade after the fact rather than preventing
one.
2026-10-19 09:17:26 +02:00
joel.poyet 2387d27f2c Record the self-hosted-runner trade-off as an accepted risk
unit-tests.yml moving to [self-hosted, macOS] on a pull_request trigger lets
any PR run arbitrary code on novaloop-mac-studio-001 — the same persistent
machine, keychain and signing identities the release pipeline uses. The risk
was accepted on the strength of the repo being INTERNAL, but that reasoning
lived nowhere: nothing in the repo said why it was tolerable, and nothing in
CI would ever say it existed.

Worth recording because the trade-off is invisible to the required checks.
zizmor reports self-hosted-runner only at --persona=auditor; the default
persona CI runs suppresses it, so a green build is not evidence of absence.
Verified both halves against the fix-fork-ci workflow: auditor reports it at
medium severity, the default reports "No findings to report … (3 suppressed)".

Records the exits too, so a future reader inheriting this need not rederive
them: restrict the trigger to non-fork branches, or return to GitHub-hosted
macos-26 with a raised timeout-minutes.
2026-10-19 09:17:26 +02:00
joel.poyet d9a57e0c03 Resync the release plan's embedded workflow and skill with the shipped files
The plan embeds verbatim copies of two shipped files and both had drifted. The
shipped files are correct, so the copies are now byte-identical to them
(verified by cmp against `git show`).

Task 8's workflow still spliced `${{ github.event.inputs.version }}` straight
into the shell, so anyone re-deriving the workflow from the plan reintroduced a
script injection that the regex below it cannot prevent — the input is
interpolated before the check ever runs. It now reads the input via the
`INPUT_VERSION` env var, and gains the `Record toolchain` step that is a
release's only record of which compiler built it now that nothing pins Xcode.

Task 11's skill claimed deleting the app clears `hasSignedInBefore`. It does
not: the flag lives in the shared `group.ch.healthchat` UserDefaults suite,
which survives deletion, so that advice yields a void migration test that looks
like a pass — the precise false negative the section exists to prevent.

Task 3 Step 5 carried the same fault in a subtler form: it matched the bare
substring "skipping loadAccounts", but two lines in
`AuthenticationService.swift` (:52 and :57) end that way and mean opposite
things. Reading them as one told you to dismiss "Classic App not configured" —
migration broken for every user — as a void run. Step 1 already prescribed the
correct remedy and is unchanged.
2026-10-19 09:17:26 +02:00
joel.poyet d7a93dddea Correct the release plan and design for the runner's measured state
Both documents told the reader to pin Xcode 26.4 and to install it via a
Brewfile. A probe of novaloop-mac-studio-001 refutes the premise: it carries
exactly one Xcode, 26.6, globally selected, with no versioned siblings and no
26.4 anywhere. The design justified the pin with "the runner has it and the
legacy pipeline proves it works" — both halves are false, and a pin naming an
absent version does not fail fast: xcodes starts a multi-GB authenticated
download that no CI secret can satisfy, so before_all hangs.

Record the no-pin decision and its cost — whatever Xcode the runner has is what
ships, unrecorded — rather than only deleting the wrong instruction, so nobody
reintroduces the pin from the same reasoning. Drop the Brewfile with it: its only
purpose was installing xcodes for that pin, and brew has no business running on a
shared release machine where it could upgrade xcodegen mid-release.

Add the runner's real constraints (no passwordless sudo, single Xcode, five
missing tools installed by hand) to the design, since they are why upstream's
setup_github_actions_environment cannot run here.

Two new risks. The legacy element-ios pipeline pins the same absent 26.4, so it
is very likely broken right now — not this fork's problem to fix, but the human
needs to know before the next legacy release. And with no pin, an Xcode upgrade
on the runner silently changes the compiler for every subsequent release.

The plan's embedded Fastfile and skill now match the branches that implement
them byte-for-byte, so the plan cannot drift from the artefacts it describes.
2026-10-19 09:17:26 +02:00
joel.poyet ae2b32217b Sync the plan's skill block with the corrected guard wording 2026-10-19 09:17:26 +02:00
joel.poyet 532ed17204 Correct when the version guard fires relative to xcodes
Task 6 Step 3 asserted the guard must fail before xcodes runs. It cannot:
fastlane's before_all always executes before the lane body, and the plan puts
xcodes in before_all and the guard in the lane. The acceptance criterion was
unmeetable on any machine.

The guard still does what it exists for — aborting before the ~30 minutes of
signing — so the wording was wrong, not the pipeline. Also record that the step
needs the xcodes CLI, since without it the lane dies earlier with an error that
reads like a config problem.
2026-10-19 09:17:25 +02:00
joel.poyet b9f1e0fbda Write the release skill out in full in the plan
Task 11 described the skill's sections rather than showing them, the one place
the plan told an implementer what to write instead of what to type. Replace it
with the complete SKILL.md.

Writing it surfaced a broken convention worth not copying: the legacy skill
documents healthchat-v1.5.0-rc1 style tag suffixes, but the workflow derives the
marketing version by stripping only the healthchat-v prefix, so the suffix reaches
CFBundleShortVersionString, which Apple caps at three integers. Tags are numeric
only here, and test builds go through workflow_dispatch with the marker in the
release notes.
2026-10-19 09:17:25 +02:00
joel.poyet 8c6e4169a8 Implementation plan for HealthChat App Store release
Task-by-task plan for the three PRs in the release design: the classic migration
fix, the fastlane pipeline, and the releasing-healthchat skill.

The migration fix gets a real test cycle — an existing upstream test,
classicAppAccountRequiresBackup, encodes the behaviour we're changing, so it is
rewritten first and drives the view model change.
2026-10-19 09:17:25 +02:00
joel.poyet 2835a5b10d Design for HealthChat App Store release capability
Spec for bringing App Store release capability to element-x-ios, targeting the
existing HealthChat record that the legacy element-ios fork ships to today.

Both apps share bundle id ch.healthchat.element and team 6G37K72W82, so element-x
arrives as an in-place update rather than a new listing. That drives the design:
the identity already aligns and 26.07.3 clears the legacy's 1.4.1, but the repo
has no release machinery at all, and upstream's Xcode Cloud path is unavailable
because it needs the private Enterprise submodule.

Records three parts: repointing CLASSIC_APP_* at HealthChat's own identifiers so
existing users keep their sessions and encrypted history, a fork-owned fastlane
pipeline ported from the legacy fork, and a releasing-healthchat skill.

Revised after an adversarial review of every factual claim; 26 issues found, none
rejected. Most consequential: the migration fix is not monotonic, because making
loadAccounts() succeed exposes a backup-instructions sheet whose only button opens
element://open — a scheme this app does not register and whose legacy owner has
been replaced in place.
2026-10-19 09:17:25 +02:00
joel.poyet 3d1fa27a4b Rebrand as HealthChat and pin the account provider to healthchat.ch
Applies the HealthChat product identity through the fork seams upstream already
provides, so no upstream logic files are touched and merges stay clean.

- Variants/HealthChat/healthchat.yml holds the identity scalars (display name,
  bundle id ch.healthchat.element, app group, development team) and is included
  permanently from project.yml. APP_NAME stays ElementX so the module name and
  @testable import ElementX keep working.
- ElementX/Sources/AppHooks/HealthChat/ registers an AppSettings hook pinning
  accountProviders to healthchat.ch with other providers disabled, push through
  sygnal.healthchat.ch and a custom-scheme OIDC redirect; plus a Compound hook
  recolouring the accent token family to the HealthChat blue.
- Entitlements are edited directly in ElementX/SupportingFiles/target.yml rather
  than in the variant, because XcodeGen concatenates arrays on include-merge and
  a variant can override scalars but cannot remove array entries. Drops the
  Element Classic app group and keychain group, and narrows associated domains
  to app.healthchat.ch, keeping matrix.to for permalinks.
- Info.plist CFBundleName now follows APP_DISPLAY_NAME — it, not
  CFBundleDisplayName, is what the web authentication sign-in alert shows.
- App logo and app icon assets swapped in place; the onboarding welcome title
  moves to a new screen_onboarding_welcome_title_ios untranslated string.
2026-10-19 09:17:24 +02:00
Valere c6d21dcd7f Bump element-call-ios to 0.1.0-rc.11 and the Matrix Rust SDK to 26.10.15
element-call-ios 0.1.0-rc.11 needs the SDK's 26.10.15 widget
capabilities, and brings matrix-rust-rtc 0.5.0-rc.5. The SDK drops
Room.roomEventsDebugString, whose mock goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:28:37 +03:00
Valere d33dc058c2 Report native calls that end without us to CallKit
The call package now says why a call ended. Only our own hang-up is an
end call action; a call the other side declined or left, that nobody
answered, or that failed is reported to CallKit with that reason, so the
call log no longer shows it as one we ended, and no end action comes back
to hang up a call that is already over.

Also adopts the package's other host changes: the voice call icon beside
"Calling…", and the call's large avatar rendered at the 140 pt it is
drawn at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:28:37 +03:00
Mauro 268d6c8ead Notify every signed-in account and turn notifications on and off per account (#6337)
* Store the notifications switch per account

Adds AccountSettings.enableNotifications and moves the app-wide value into the signed in accounts before they're restored: the first account takes it out of the app settings and the others get the same switch, so someone who turned notifications off keeps them off.

* Add a notification service to every user session

Each account's NotificationService sets its pusher when its notifications are on and deletes it when they're off (appending with multiple accounts), keeps the account's unread count and decides which of its delivered notifications to clear. It asks the NotificationManager, the only owner of the notification center, through actions: remove the notifications matching a rule, request authorization and the unread count. The UserSessionManager connects every live session's service to the manager, and flows and screens use the session's service instead of the manager, which leaves the flow parameters.

* Route notifications by account

Taps only open rooms of the active account for now, a tap made while restoring only opens once its account is the active one, replies are sent from the account they were made for, and signing out only clears that account's notifications unless it was the last one. The notification extension hides pushes for an account that signed out while other accounts are still signed in.

* Turn notifications on and off for each account

The notification settings switch is now saved per account, and each account's NotificationService sets or deletes its pusher when it changes, asking for permission when it's turned on.

* Apply review suggestions to multi-account notifications

Migration
- Move the notifications switch with a single per-account migration, like 26.10.1 and 26.10.2, and drop its dedicated test.

Notification manager and service
- Follow each account's badge count publisher directly, removing the badge count action and the stored counts.
- Only reset the badge and unregister from APNs when the last account signs out, as each account already clears its own notifications.
- Set up the service's subscriptions in a single setupSubscriptions().
- Rename switchChanged(to:) to enableNotificationsChanged(to:) and pusherConfiguration(identifiers:) to pusherConfiguration(for:).
- Build the service without hopping to the main actor, it already runs there.

Notification service extension
- Check the account's switch in NotificationHandler using the session's settings, and log an error when a push arrives for an account that's turned off.
- Always show pushes that match no signed-in account and log an error, so a broken setup stays visible.

Other
- Store a pending inline reply in a PendingInlineReplyDetails struct.
- Assign the chats tab badge with weakAssign.
- Make PusherConfiguration Equatable so the pusher test checks it in one expectation.
- Clarify the comment about notifications without a receiver.

* Print instead of logging before the app hooks run in the NSE

A push that matches no signed-in account goes back to printing, as MXLog shouldn't be used before the app hooks have run. The push is still shown so a broken setup stays visible.
2026-10-15 13:10:26 +02:00
Mauro Romito 09ecec4306 Ignore repeated confirms on the server selection screen
Tapping Continue and then the keyboard's Done key (or double tapping
Continue) started two server configurations. With a password-only server
both asked the flow to show the login screen, and the second request hit
the authentication state machine's unexpected transition fatalError.

Keep the running confirmation task and ignore further confirms until it
finishes, so the flow continues once. The guard clears afterwards so
confirming again after going back from the login screen still works.

Fixes ELEMENT-X-IOS-XED
2026-10-15 12:34:50 +02:00
Stefan Ceriu 11651bcd23 Normalize generated schemes to match Xcode's output
XcodeGen (through XcodeProj) serializes schemes slightly differently to
Xcode, so Xcode rewrites them as soon as it touches them (running tests,
switching test plans, opening the scheme editor), leaving recurring local
changes behind.

Add a small Swift script, run from XcodeGen's postGenCommand, that rewrites
the generated schemes the way Xcode saves them: it drops default-valued
attributes and empty CommandLineArguments, orders TestPlanReference
attributes, and moves TestPlans to the end of the TestAction, omitting the
redundant empty Testables.

Regenerated schemes are byte-identical to the copies saved by Xcode. The
script can be removed once https://github.com/tuist/XcodeProj/pull/1189
ships in XcodeGen.
2026-10-15 12:54:48 +03:00
Doug b6ccbbc6c3 Add a new UserHooks container and move some AppHooks. (#6332)
* Add a UserHooks system with just the CreateRoomScreenHook for now.

* Move more hooks into UserHooks.

* Pass the ClientProxy to makeUserHooks instead of calling AppHooks.configure(with:).

* Update the Enterprise submodule.
2026-10-15 10:49:27 +01:00
renovate[bot] 11b6cc3f70 Update actions/upload-artifact action to v7.0.2 2026-10-15 12:13:54 +03:00
Stefan Ceriu 27e59a81e4 Fix integration tests following the search tab introduction
Tabbar tabs now can take accessibility identifiers and the integration test uses the newly expected flow.
2026-10-14 20:14:27 +03:00
Mauro 7752ae3f6a Keep every signed-in account live (#6299)
* Keep every signed-in account live

With the multi-account flag on, the accounts other than the active one are now
restored at launch, one after the other once the active account's UI is up.
They run their migrations, sync while the app is active, get a pusher (appended
so accounts on the same homeserver keep each other's) and count towards the app
badge, and background refresh and search backfill cover them too. Only the
active account's remote settings are ever applied.

Calls stay with the active account until the call service handles every account,
and an auth error on another account is only logged until accounts can be signed
out one at a time. With the flag off only the active account is live, as before.

* Sum the app badge across every signed in account

Each live account now reports its own unread count, and the app badge
shows their sum. Delivered notifications are only cleared for the
account whose rooms were read, so a room shared by two accounts keeps
the other account's notifications.

The room list observer for the badge and the notifications moves from
the UserSessionFlowCoordinator to the AppCoordinator's per-account
wiring. The flow keeps updating the chats tab badge.

* Let the user session manager manage every session

- The AppCoordinator tells the manager to pause, resume, go offline or
  run the search backfill, instead of looping over the sessions itself.
- The manager also restores the other accounts, preparing each with the
  AppCoordinator's migrations and wiring, and resumes them while the
  services are running. With a single account nothing changes.
- Wiring an account again (e.g. after clearing the cache) replaces its
  observers rather than adding to them.
- Add addAccountFlowEnabled, a developer-only switch for the upcoming
  add account flow. It isn't a user preference, so testers enabling
  multi-account can't add a second account yet.

* Apply review suggestions to keeping every account live

- Move the notification changes (pushers for every account, the summed
  app badge and per-account clearing) to their own PR, so notifications
  follow the active account again for now.
- Revert loading only the active account's remote settings: they load in
  the store again, as on develop, until the hooks are reworked.
- The user session manager reports the auth errors of every live session,
  so the AppCoordinator subscribes once instead of keeping per-account
  observers.
- Restoring a single account is private to the manager now.
- Rename otherSessionsRestoreTask to otherSessionsRestorationTask.

* Restore every signed in account through one path

The user session manager now restores every account in a single pass,
most recently selected first, instead of separate restores for the
active account and the others. The AppCoordinator shows the first
account that comes back and no longer reads the multi-account flag:
without it, the manager only ever makes one account live.

- Clearing the cache clears every live account, and the next pass
  replaces their sessions.
- Each account is migrated once per launch, and an account that just
  signed in is never migrated.
- A pass also runs after signing in, so the other accounts come live.
- Remove UserSessionManagerError, restoreActiveSession and
  restoreOtherSessions.

* Only clear the cache of the active account

Clearing the cache goes back to clearing the account shown in the UI.
Its session stays live until the next restore pass replaces it, and when
that restore fails another live account becomes the active one.

* Apply review suggestions to restoring every account

- Remove the search backfill guard: with no sessions the manager starts
  nothing and reports that nothing is running, so the task completes.
- Add a TODO to switch to the most recently used account when the
  active one signs out while others are signed in (element-meta#3325).
- Disable SwiftLint's type_body_length on the AppCoordinator again, it's
  just over the limit with develop's new migrations.
2026-10-13 17:06:05 +00:00
Danny ae9e0cc176 Remove unused room details hook support (#6325)
* Remove unused room details hook support

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>

* Update the Enterprise submodule.
2026-10-13 16:19:45 +00:00
Stefan Ceriu 900b24bc5e Remove unnecessary iOS 26+ search screen restriction. 2026-10-13 19:00:43 +03:00
Stefan Ceriu 95fc43f4fc Bump the RustSDK to v26.10.13 2026-10-13 19:00:43 +03:00
Mauro b31950789f Move synchronous image upload preparation off the main actor (#6323)
* Move synchronous image upload preparation off the main actor

Signed-off-by: Ivan Magda <imagda15@gmail.com>

* Make media preparation cooperative with cancellation

Replace the dispatch queue with concurrent copy, image and video-thumbnail
functions. Return sendable upload metadata directly, bound each batch to
two preparations and discard unfinished results when cancellation occurs.

Keep file metadata reads synchronous and prevent the preview from sending
a result after its preparation task has been cancelled. Replace unrelated
tests with cancellation and batch-order coverage requested during review.

* Update MediaUploadingPreprocessor.swift

* Run media preparation in a nonisolated preprocessor

Make MediaUploadingPreprocessor a nonisolated struct whose copy, image
processing and video thumbnail run in @concurrent instance methods,
instead of a main-actor type with static helpers and an extra setting
parameter.

Check cancellation with plain early returns between the slow steps,
name the batch limit, stop logging cancelled video exports as errors,
and restore develop's structure and doc comments where they were changed
without need.

Replace the tests that didn't reach the new code with tests for cleanup
on failure, on batch failure and on cancellation, and make the preview
test set up its blocking mock before the view model starts processing.

* Apply review suggestions to media preparation

Docs:
- Drop the doc comment on maximumConcurrentProcessingCount, the name says it.
- Move the cleanup note into the main description of both processMedia
  functions, so the Returns lines match develop again.

Tests:
- Rename the test helper to copyResourceToTemporaryDirectory(_:).

---------

Signed-off-by: Ivan Magda <imagda15@gmail.com>
Co-authored-by: Ivan Magda <imagda15@gmail.com>
2026-10-13 17:33:22 +02:00
Doug e1780f322a Remove un-necessary plan file. 2026-10-13 15:24:26 +01:00
Doug ac29354b08 Move any visible account settings into AccountSettings. (#6308)
* Remove the now unused setting to disable in-app notifications.

* Move visible account settings into AccountSettings.

Have ignored enableNotifications for now as that will require reworking the NotificationManager.

* Allow different sounds to be selected for each account.

* Move defaultNotificationTone into AccountSettings.
2026-10-13 15:21:13 +01:00
Stefan Ceriu 011087baf5 Adopt correct UIAA and session token passing usage as per spec. 2026-10-12 22:38:31 +03:00
Stefan Ceriu 8f54e6159d Bump the RustSDK to v26.10.12 2026-10-12 22:38:31 +03:00
Ivan Magda ad88f0da7c Update room suggestions and search when space membership changes (#6316)
* Avoid repeated space membership scans in room search

Signed-off-by: Ivan Magda <imagda15@gmail.com>

* Refresh room candidates when space membership changes

Keep known space room IDs in a set updated by the room publisher and by
successful additions. Filter both suggestions and search results through
that set, retaining exclusions while the space list resets and reloads.

Preserve the initial recent-room eligibility filter so existing children
do not consume the five suggestion slots. Cover live membership updates
and partial-save filtering in both candidate lists.

---------

Signed-off-by: Ivan Magda <imagda15@gmail.com>
2026-10-12 14:03:44 +00:00
Mauro Romito f99b607f08 Fix room pickers not loading more than the first page of rooms
The forward, share, add-to-space and search pickers signal reaching the
bottom of the list with an empty `roomCount..<roomCount` range. The
prefetch sink added in 9dd9fd917 guards on `!range.isEmpty` to skip the
initial `0..<0`, which also dropped the pickers' bottom signal, so they
never loaded past the first 100 rooms.

Only skip `0..<0` instead, which leaves the home screen prefetch as it
was, and cover the bottom, prefetch and top cases in
RoomSummaryProviderTests.
2026-10-12 13:47:17 +02:00
Mauro 1609bbbf81 Ask for the model and harness used in AI-assisted pull requests (#6321)
* Ask for the model and harness used in AI-assisted PRs

Add an AGENTS.md rule that agents name the model and harness in the PR
description, and a matching line under the LLM checkbox in the pull
request template so human contributors fill it in too.

* Only let agents fill the LLM line of the PR template

Agents leave the description template to the developer and only fill in
the LLM line with the model and harness used.
2026-10-12 11:56:14 +02:00
Stefan Ceriu 60c77c6320 Fix one missed UI test snapshot. 2026-10-12 12:54:17 +03:00
Mauro Romito 26eecaf3d2 Update encryption reset snapshots
Re-recorded for the new Localazy copy: the "Continue" button and the updated third bullet.
2026-10-12 10:24:01 +02:00
Element CI 8df22d7106 Translations update 2026-10-12 10:24:01 +02:00
Stefan Ceriu eb56586e76 Fix UI test snapshots after enabling the new search tab. 2026-10-12 10:17:46 +03:00
Stefan Ceriu 14e33866ce Enable the new search experience by default 2026-10-09 20:34:32 +03:00
Valere Fedronic ecced8ee48 Bump element-call-ios to 0.1.0-rc.10 (#6303)
* Bump element-call-ios to 0.1.0-rc.10

The release moves the RTC core to matrix-rust-rtc's room-first API, so the
stack no longer needs starting or stopping: it subscribes to a room when a
call opens it. It also adds headphones and Bluetooth audio output icons,
mapped here to Compound's headphones icon.

* Bump compound-design-tokens to 11.3.1 and use the Bluetooth output icon

11.3.1 adds the `bluetoothOutput` icon, replacing the `headphonesSolid`
placeholder for the Bluetooth audio route in native calls.
2026-10-09 15:16:13 +01:00
Mauro Romito 5792a8ba5c Create sessions through the user session manager
Sign-in now creates and stores its session through the
UserSessionManagerProtocol rather than the UserSessionStore, so the
AppCoordinator no longer holds the store.

- The manager forwards clientSessionDelegate and
  userSession(for:sessionDirectories:passphrase:) to its store. As with
  restoring, the new session isn't registered until the AppCoordinator
  adds it.
- AuthenticationService takes the manager, for both sign-in and soft
  logout.
- The store mock's configuration moves to the manager mock.
2026-10-09 15:21:27 +02:00
Mauro 60db364436 Introduce a user session manager (#6295) 2026-10-09 14:07:39 +02:00
Doug 65bab49f39 Remove the CommonSettingsProtocol and use UserSession to carry multiple dependencies. (#6302)
* Remove the CommonSettingsProtocol.

* Replace multiple dependencies with the UserSession when it carries them.
2026-10-09 10:20:00 +01:00
Stefan Ceriu a3d170534c Update snapshots 2026-10-09 11:44:39 +03:00
Stefan Ceriu 8b8384a108 Remove unnecessary type_body_length AppCoordinator SwiftLint rule 2026-10-09 11:44:39 +03:00
Stefan Ceriu 46eb23fa64 Show the unread notification count on the chats tab
Set the chats tab's badge count from the same value used for the app badge.
The tab bar already renders it through the .badge modifier but that has no
effect outside of a TabView or List, so the iPad tab rail now overlays a
small NotificationCountBadge instead.
2026-10-09 11:44:39 +03:00
Stefan Ceriu ab39d8de06 Update the app badge and clear read notifications on room list updates
The sync update action is sent from the room list service state listener,
which fires before the SDK has applied the new unread counts, leaving the
app badge one sync behind. Drive both the badge and the clearing of
delivered notifications for fully read rooms from the room list publisher
instead, moving the observer from the ChatsTabFlowCoordinator into the
UserSessionFlowCoordinator.

The NotificationManager now receives the badge count from the caller so the
same value can be reused elsewhere.
2026-10-09 11:44:39 +03:00
Stefan Ceriu 61fe2a8158 Add a shared NotificationCountBadge view
Extract the unread notification count capsule from the room list cell into
its own view so it can be reused elsewhere. It caps the displayed value at
99+ and comes in a medium size (used by the room list) and a small size.
2026-10-09 11:44:39 +03:00
Stefan Ceriu 2182446684 Clear notifications for fully read rooms using the event's timestamp instead of delivery date 2026-10-09 08:52:56 +03:00
Doug fc75f90ef5 Remove the dynamic member lookup from UserSettings in favour of nested access. (#6294)
* Remove the dynamic member lookup from UserSettings in favour of nested access.

* Remove forwarding methods from UserSettings.

* Update the Enterprise submodule.
2026-10-08 15:19:44 +01:00
Mauro Romito fba28ed422 Fix the crash when signing back in after a soft logout
The state machine routes `.createdUserSession` from `.softLogout` to
`.signedIn`, but the AppCoordinator transition handler had no case for
it, so signing back in from the soft logout screen hit the
`fatalError("Unknown transition")` default.

Set up the user session as a restored one (`isNewLogin: false`): the
soft logout login reuses the same device ID and crypto store, and the
onboarding flow still starts if anything is outstanding.
2026-10-08 15:14:32 +02:00
Doug c296659ad6 Only show media captions in QuickLook once. (#6287) 2026-10-08 11:10:28 +01:00
Stefan Ceriu fb68a578ae Limit the Renovate schedule to the early hours of the scheduled Wednesday
The "every 2 weeks on wednesday" schedule opened a window for the whole day,
so after the group PR was merged Renovate kept running and raised new PRs on
the same branch for versions that passed minimumReleaseAge later that day
(e.g. #6282 followed by #6286 and #6293).

Restrict every schedule to before 4:00 am UTC so each group produces a single
PR per cycle. The "4:00 am" form is required, later.js silently drops the time
constraint for "4am" in this position.
2026-10-08 12:04:08 +03:00
Doug 3b40aadc98 Re-organise SettingsScreen to have distinct Account/App settings. (#6288)
* Re-organise SettingsScreen to have distinct Account/App settings.

* Remove the AdvancedSettingsScreen.
2026-10-07 16:51:28 +01:00
Stefan Ceriu dafda65cbe Start the app coordinator from background tasks
A background launch doesn't always connect a scene, so nothing started
the session restore and both background app refresh and search backfill
gave up without a session.

Store optimizations are skipped while in the background, as vacuuming can
outlast the task and get the app killed before the remaining migrations run.
2026-10-07 18:21:55 +03:00
Mauro f2eaa8e80d Show the multi-account announcement on the HomeScreen (#6265)
* Present the multi-account announcement when nothing else is showing

The user session flow owns onboarding, verification and settings, so it presents the announcement in the tab sheet slot with the lowest priority: once the verification state is known, and whenever onboarding, settings or a verification sheet is dismissed. Routes and sign-out dismiss settings without triggering it, and it is marked as seen when presented.

* Show the multi-account announcement on the home screen

Following review, the announcement is a best-effort sheet on the home
screen instead of a UserSessionFlowCoordinator state. It's presented when
the room list appears with the flag on, the announcement armed and the
session verified, and counts as seen once its content appears.

- Restore UserSessionFlowCoordinator to develop.
- HomeScreenViewModel presents the announcement through a binding and
  waits for a known verification state, so it never competes with the
  identity confirmation onboarding.
- Add account only closes the sheet until the add account flow exists.
- Rename MultiAccountAnnouncementScreen to MultiAccountAnnouncementView
  and drop its unused coordinator and view model protocol.
- Size the sheet from the measured navigation bar height and match the
  Figma spacing; the hard-coded 44pt pushed the content under the button.

* Present the multi-account announcement from the home screen view model

The announcement no longer needs its own view model: the view takes the
home screen's context and presents through a boolean binding, like the
home screen banners.

- Drop MultiAccountAnnouncementViewModel and its models. The view sends
  home screen actions to close or add an account and builds its own
  attributed subtitle.
- Check the verification state when the room list appears instead of
  waiting for it. Restored sessions already know it by then, unverified
  ones get the identity confirmation cover first and the room list
  appears again once it's dismissed. Nothing is left subscribed, so the
  sheet can no longer turn up later wherever the user is.
2026-10-07 12:29:51 +02:00
renovate[bot] 82a933d1d8 Update Project Dependencies (#6282)
* Update Project Dependencies

* Enable MetricKit for app hang diagnostics.

* Ignore the new first-party Call packages from our acknowledgements.
2026-10-07 10:15:50 +00:00
Doug cf25287308 Add a LocationSharingSettingsScreen. (#6281) 2026-10-07 09:10:15 +01:00
Doug ce3829f253 Add a MediaUploadQualitySettingsScreen. (#6279) 2026-10-07 09:10:14 +01:00
Doug 51dd2efe23 Add a ModerationAndSafetySettingsScreen. (#6278) 2026-10-07 09:10:14 +01:00
renovate[bot] 09085c8bb8 Update dependency element-hq/compound-design-tokens to v11.1.0 2026-10-07 08:45:48 +02:00
Doug 93825c01ad Initial implementation of AccountSettings. (#6275)
* Make the UserPreference macro also generate an accessor for the key name.

* Initial implementation of AccountSettings.

- Add an AccountUserDefaults storage with a userID prefix on the key.
- Migrate the 2 values reset by AppSettings.resetSessionSpecificSettings to AccountSettings.

* Rename UserSettings.volatile() to UserSettings.mock()

Given it now has a userID parameter with a default value..
2026-10-06 17:52:24 +01:00
Stefan Ceriu acd18f3aa9 Fix location sharing double sheet presentation on the Mac
Which would override each other and break the UI state
2026-10-06 17:40:19 +03:00
mxandreas 4640f9cec0 Add hooks for room creation and privacy & security settings screen. (#6270)
Add hooks for the create room and security and privacy screens.
2026-10-06 09:05:49 +00:00
Mauro 60c11c8172 Add the multi-account announcement screen (#6263)
* Add the multi-account announcement screen

A self-sizing bottom sheet telling users they can now add multiple accounts, matching the designs. Close and Add account both report back to the coordinator; nothing presents it yet.

* Use the Localazy strings for the multi-account announcement

The copy now lives in Localazy as screen.multi_account_announcement.*, named after the screen rather than the room list.

* Apply review suggestions to the multi-account announcement

- Use an ElementNavigationStack with a toolbar close button, like RedactConfirmationView, instead of an overlaid glass button.

* Remove the large text preview from the multi-account announcement

Other sheets built the same way don't snapshot Dynamic Type sizes, and the iOS 26 bar material it would show isn't rendered in snapshots.

* Use TitleAndIcon for the multi-account announcement

TitleAndIcon now takes an attributed subtitle (#6274), so the sheet no longer needs to rebuild its layout by hand. The snapshots are unchanged.
2026-10-05 18:03:19 +02:00
Mauro Romito 1b06ca0dcd Let TitleAndIcon take an attributed subtitle
Add an AttributedString subtitle initialiser to Compound's TitleAndIcon,
with the existing String initialiser forwarding to it so current call
sites are unchanged. Add a preview state with a bold run and re-record
the titleAndIcon snapshots.

This lets screens with partially bold copy, such as the multi-account
announcement, use TitleAndIcon instead of rebuilding its layout.
2026-10-05 16:04:22 +02:00
Mauro 8a6e297cba Media preview single stale refresh (#6272) 2026-10-05 14:35:16 +02:00
Mauro 49457f7ae8 Multi account feature flag (#6261)
* Add a multi-account feature flag

The flag lives in Developer options. Switching it on arms the upcoming one-time multi-account announcement, whose seen state defaults to true so fresh installs never see it.

* Add an Add account row to Settings behind the multi-account flag

The row sits in its own section under the user status, as in the designs. Tapping it is routed up to the settings flow, which ignores it until the add account flow exists.

* Remove the Developer options view model tests

The Developer options screen is excluded from coverage, so these tests don't add value.

* Apply review suggestions to the multi-account flag

- Order the multi-account requirements in DeveloperOptionsProtocol to match the screen's sections.
- Drop the redundant "Experimental" from the toggle's subtitle.
2026-10-05 13:04:16 +02:00
Doug 9ce3bd97d8 Move the status emoji placement on the home screen. (#6260)
* Minor tweaks to User Status.

- Change the layout of the status emoji on the Setting button.
- Remove the status emoji from the Settings screen (it's shown directly below in the status row).
2026-10-05 10:18:23 +01:00
ElementRobot 57f5498da0 Translations update (#6268)
Co-authored-by: Element CI <ci@element.io>
2026-10-05 10:01:50 +01:00
Stefan Ceriu 9f141585a2 Focus events on the live timeline when it has already loaded them
Opening a room on an event always built a detached timeline, even when the
live timeline already contained that event, e.g. when tapping the notification
for the newest message in a room.

The timeline controller now stays on the live timeline in that case, scrolling
to and highlighting the event, and only builds a detached timeline when the
event isn't loaded.
2026-10-02 16:52:19 +03:00
Stefan Ceriu 52b411e954 Revert "Open the room live when tapping the notification for its newest message"
This reverts commit 4c71cae0f.

Comparing against other delivered notifications doesn't reflect the timeline:
in rooms set to mentions and keywords, after swiping away newer notifications
or with messages sent from another device, newer messages can exist without a
notification, so the room opened live with the tapped event out of view.
2026-10-02 16:52:19 +03:00
Stefan Ceriu a50c80c179 Avoid flashing the room list skeletons on launch
On a cold launch the home screen appeared just before the cached rooms
published, briefly showing skeletons that were immediately replaced.

The home screen now starts in a new awaitingCachedRooms mode, keeping
the list blank while a cached room list is about to publish and falling
back to the skeletons after 500ms. Without a cache the skeletons show
straight away as before.
2026-10-02 16:37:22 +03:00
Mauro 4b6953e80f Point agents to Apple's Xcode agent skills in AGENTS.md (#6256) 2026-10-02 15:00:16 +02:00
Copilot c41a38159b Only offer joined rooms in the forward and share pickers (#6255)
* Initial plan

* Offer only joined rooms in destination pickers

Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>

* Clean whitespace in share picker test

Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>

* Restrict the shared picker room list to joined rooms

The alternate room summary provider, which backs the forward, share and
Add rooms pickers and global search, now always includes the SDK's
joined filter in its base filter. This replaces the per-call joinedOnly
flags, so Add rooms no longer offers invited rooms or spaces and no
screen can forget the flag.

A filter set before the provider has its room list is now applied once
the list is set, instead of being reset to the default. The share picker
hit this on a cold launch from the share extension.

The forward and share pickers also hide upgraded rooms, matching the
Suggestions section. The SDK has no filter for these.

Also registers the share picker's test file in the project, fixes the
blank-line indentation SwiftFormat flagged, and re-records the picker
snapshots without the upgraded room.

* Check room membership for forwarding suggestions and search breadcrumbs

RoomSummary now has isJoined, built from the room's membership. The
forwarding suggestions and the search breadcrumbs used a missing
joinRequestType to mean joined, which is also the case for rooms that
have been left or banned from. It only worked because the room list's
base filter already drops those.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-10-02 12:44:33 +00:00
Stefan Ceriu 4fe58c2d0a Don't hold the room list on skeletons for an empty filtered result
The room list keeps showing skeletons while the provider reports loaded
but no summaries have published yet, to avoid flashing an empty list
before the first batch lands. That guard also fired when a filter
(chips, space or search) legitimately matched nothing, leaving the home
screen on placeholders forever instead of showing the empty filter
state.
2026-10-02 15:11:22 +03:00
Mauro d3604e8115 Announce toasts with VoiceOver (#6250)
* Announce toasts with VoiceOver

Toasts are posted from a separate overlay window and never take
VoiceOver focus, so their text was never spoken. UserIndicatorController
now posts a high priority announcement with the title whenever a toast
with readable text is submitted, including re-submissions of an already
visible toast with the same ID. Modal indicators are not announced.

* Remove empty-title toast announcement check

Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>

* Hide toasts from VoiceOver

The toast title is now announced by UserIndicatorController when the
toast is submitted, so letting VoiceOver also focus the toast itself
would only read the same text twice.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-10-02 12:29:18 +02:00
Mauro f91b7441e7 Suggest recently visited rooms when forwarding messages (#6251)
* Suggest recently visited rooms when forwarding messages

The Forward to screen now shows up to 5 recently visited rooms in a
Suggestions section, above a Chats section that lists every room. The
suggestions are never searched: while searching, only the Chats section
is shown and filtered. The source room, invites, spaces and upgraded
rooms are never suggested, and the section is hidden when there is
nothing to suggest.

The suggestions are resolved from the room summaries rather than room
proxies, so they show up without holding back the list, and they are
re-resolved as the room list loads after a cold start. ClientProxy now
exposes the recently visited room IDs for this.

The recently visited rooms filter is now async, so callers choose its
isolation instead of asserting it at runtime.

* Apply review suggestions to the forwarding suggestions

Rooms:
- Each room is listed once. Suggested rooms are left out of Chats unless
  searching, where Chats lists every result. This also removes the
  duplicated row whose selection changed without the tapped row's
  animation.
- The Chats header is hidden when the section has no rooms.

Suggestions:
- Resolve them once when the screen opens instead of on every room list
  update.
2026-10-02 12:04:07 +02:00
Mauro Romito e67a4c388f Read the number of selected messages to VoiceOver
The count was only on the forward button at the bottom, so VoiceOver
users had to go and find it after every tap. While selecting, each
selectable message now starts its label with "N messages selected".
The label changes on every toggle, so VoiceOver reads the new count
followed by the message, as in other messaging apps.
2026-10-01 21:39:47 +02:00
Mauro Romito 8ddd161625 Keep message selection active when every message is unselected
Unselecting the last message used to end the selection, closing it
without the user asking. Selection mode is now its own state: Forward
starts it and it only ends with the close button, once the messages are
forwarded, when the timeline changes or the feature is turned off. With
nothing selected the forward button stays visible but disabled.
2026-10-01 17:43:42 +02:00
Stefan Ceriu cdb5d4ee31 Group dependencies shared by Compound and the app in one Renovate PR
swift-snapshot-testing is pinned in both compound-ios/Package.swift and
project.yml. Renovate updated them from different groups, so one side got
bumped without the other and the Xcode build broke on the mismatched
Package.resolved. A dedicated "Shared Dependencies" group updates both
together, and since that PR touches project.yml the XcodeGen workflow
regenerates the project as well.
2026-10-01 16:37:26 +03:00
Stefan Ceriu 18b0975c99 Run all Renovate dependency groups on the same schedule
Tool and Compound dependencies had no schedule while project dependencies
were only updated every 2 weeks, so their PRs landed at unrelated times.
Put them all on the same biweekly Wednesday slot and rename the Compound
group to "Compound Dependencies" for consistency with the others.
2026-10-01 16:37:26 +03:00
Stefan Ceriu f084274acb Use current Renovate option names for the Compound rule
`managers` and `matchPaths` are deprecated in favour of `matchManagers`
and `matchFileNames`. Renovate was migrating them on the fly, this makes
the config match what it actually runs.
2026-10-01 16:37:26 +03:00
Stefan Ceriu a4ff68ed84 Bump swift-snapshot-testing in the main project too 2026-10-01 16:37:26 +03:00
renovate[bot] 9a62206a4f Update dependency pointfreeco/swift-snapshot-testing to v1.19.6 2026-10-01 16:37:26 +03:00
Stefan Ceriu 11dc19f7ed Await the session restore instead of polling for it
Replace the 100ms polling loop in the background app refresh with a
subject that the state machine's transition handler sets as soon as the
app is no longer restoring a session, whatever the outcome. The refresh
now awaits it once, with the same 5 second timeout, instead of waking
up repeatedly.

Move the wait back into the class body and silence type_body_length for
AppCoordinator rather than keeping a single-function extension around
to stay under the limit.
2026-10-01 15:59:42 +03:00
Stefan Ceriu a56a5c2819 Wait for the session restore in background app refresh
When iOS relaunches a terminated app for the task, the session is still
being restored asynchronously when the refresh starts. Wait up to 5
seconds for the restore to finish, bailing out early if the app isn't
restoring a session (e.g. signed out or the restore failed).

# Conflicts:
#	ElementX/Sources/Application/AppCoordinator.swift
2026-10-01 15:59:42 +03:00
Stefan Ceriu 38312b7473 Fix the search tab's layout now that its built against os27 2026-10-01 15:59:16 +03:00
Mauro Romito b65e8f6a7c Fix formatting-only edits not rendering in the timeline
Components of a formatted body were identified by their plain characters, so an edit
that only added formatting (e.g. wrapping a word in inline code) kept the same ID.
SwiftUI then reused the existing MessageText, whose @State string ignores new input,
and the old unformatted text kept rendering.

Identify components by the hash of their attributed string so formatting changes
produce a new ID.
2026-10-01 14:56:53 +02:00
Mauro Romito 7911df9537 Fix the media viewer getting stuck on "Loading more…" when swiping forward
QuickLook requests every page up front when the viewer opens. At that point the
media timeline is still empty, so every page after the tapped media is cached as
a forward pagination placeholder. Once the timeline loads it reports forward as
end reached, so no pagination happens and nothing ever replaces those cached
placeholders, leaving the viewer stuck when swiping forward.

When landing on a placeholder, or when the timeline updates, the controller now
checks whether the data source holds a different item at that index and if so
refreshes the current page. The refresh waits for any swipe to settle, as
refreshing mid-swipe breaks QLPreviewController.
2026-10-01 14:31:29 +02:00
Mauro Romito 61384f9986 Use the window scene initialiser for UIWindow in tests
UIWindow.init() is deprecated in iOS 26. AuthenticationStartScreenViewModelTests
now creates its fake window from the test host's connected scene, matching
ServerSelectionScreenViewModelTests.
2026-10-01 10:56:08 +02:00
Mauro Romito 26c417ae68 Move AlertInfo's button types out of the generic type
With Swift 6.4, the default value for AlertInfo.primaryButton warned that
T's Hashable conformance may be isolated and can't be used in a main
actor-isolated context. The nested AlertButton/AlertTextField types
don't depend on T, so move them to top level as AlertInfoButton and
AlertInfoTextField, and keep typealiases so existing call sites compile.

Constraining T to SendableMetatype isn't possible as the synthesized
Hashable conformances of our main actor-isolated alert enums are isolated.
2026-10-01 10:55:40 +02:00
Mauro 831234865b Fix Swift 6.4 warnings (#6240) 2026-10-01 10:55:19 +02:00
Mauro Romito 99c0fa5729 Title the forwarding screen "Forward to"
The forwarding screen now picks destinations for one or several
messages, so "Forward message" no longer fits. Use the common_forward_to
string from Localazy, as in the designs, and re-record its snapshots.
2026-09-30 17:40:55 +02:00
Stefan Ceriu b21d27dbbc Jump straight to notification tap and external deep link targets
Whether to animate a route was decided after resuming the client
services, by which time a notification tap or an external deep link
had already made the app active, so the user watched a navigation
animation from the room list. Read the app state as soon as the route
arrives instead, so these land directly on their target while routes
handled in the foreground keep animating.
2026-09-30 17:06:57 +03:00
Stefan Ceriu a87d65b05a Surface failed reactions as a send failure on their message
A reaction that fails to send can now block the room's send queue, but the
timeline only surfaced failures for the item itself, its edit or its
redaction. EventTimelineItemProxy.pendingSend now also falls back to our
first failed reaction, targeting it with SendTarget.reaction(key:), so the
message shows the red exclamation mark and the sending failed alert can
retry or remove it.

In-flight reactions are deliberately ignored so reacting doesn't flash a
sending indicator on the message.
2026-09-30 15:16:13 +03:00
Stefan Ceriu 4c71cae0f5 Open the room live when tapping the notification for its newest message
Tapping a message notification always opened the room on a timeline
focussed on that event, even when it was the newest message and the
live timeline already ends with it.

On tap, the notification manager now checks whether any other delivered
notification for the same room is more recent. If none is, the room is
opened live instead of focussed. Older notifications in a room keep
focussing on their event.
2026-09-30 14:50:04 +03:00
Stefan Ceriu 7973da8251 Throw from session directories and only fatal error when making sense. 2026-09-30 12:24:12 +03:00
Stefan Ceriu a8bc26dc93 Fix various UI test snapshot issues. 2026-09-30 12:23:54 +03:00
Stefan Ceriu 5a3c4ad2f5 Make the background app refresh task live in its own extension as well 2026-09-30 12:20:54 +03:00
Stefan Ceriu 49481f9af7 Add a search backfill app background processing task
To run when the device is idle, charging and connected to the network.
2026-09-30 12:20:54 +03:00
Stefan Ceriu d08b0e0090 Start a foreground search backfill request when entering the message search tab 2026-09-30 12:20:54 +03:00
Stefan Ceriu 790bf0a44e Adopt the new search backfill client interfaces. 2026-09-30 12:20:54 +03:00
Valere 8aae06d3ae Bump element-call-ios version to 0.1.0-rc.9 2026-09-30 08:20:36 +03:00
Stefan Ceriu ec4d1a3c04 Update some icons to use the primary accent color instead of tertiary for accessibility reasons. 2026-09-29 22:11:16 +03:00
Stefan Ceriu 0de6231399 Give search breadcrumbs a consistent row height
Query and room breadcrumbs reserved slightly different heights, and
rooms without a description collapsed further. Both now reserve the
space of a title and subtitle so every row lines up.
2026-09-29 21:53:04 +03:00
Stefan Ceriu a78e4f0c2a Add a clear all button to recent searches
Lets users wipe their whole search history at once from the recent
searches section header, as per the designs.
2026-09-29 21:53:04 +03:00
Stefan Ceriu 50b38555b6 Allow removing individual search breadcrumbs
Each recent search now has a trailing close button to remove it from
the history. The view action is logged without its payload so removed
queries don't end up in the logs.
2026-09-29 21:53:04 +03:00
Stefan Ceriu 313365ad83 Only record query breadcrumbs when the search is submitted
Queries were previously stored whenever a result was selected, which
meant half-typed queries ended up in the history. Now a query is only
recorded when the user submits the search field, while selecting a
result only records the room.
2026-09-29 21:53:04 +03:00
Stefan Ceriu bc018251c7 Update translations to bring in action.clear_all 2026-09-29 21:53:04 +03:00
Stefan Ceriu 1464a98ef0 Split the description of the new "Show all activity" notification setting 2026-09-29 21:36:44 +03:00
Mauro Romito e6bd8b0eb9 Fix the build after the translations update
The translations update removed screen_room_confirm_removal_message, as the redaction confirmation now asks the question in its title ("Remove message?"), matching Element X Android. Drop the message from RedactConfirmationView and re-record the previews whose copy changed.
2026-09-29 20:05:14 +03:00
Element CI dcaa2dffdb Translations update 2026-09-29 20:05:14 +03:00
Mauro b1ed870474 Support for Xcode 27 on CI (#6217)
* Run the tests on iOS 27.0

Bump the required simulator OS in the preview, UI and Compound tests, and the CI tool's default OS, from 26.5 to 27.0. Test devices stay the same.

iOS 27 writes transcoded videos 146 bytes larger, so update the expected sizes in MediaUploadingPreprocessorTests.

* Build and test on CI with Xcode 27

Select Xcode 27 in ci_common.sh and move every macOS workflow to the xcode-27 runner image, as macos-26 doesn't include Xcode 27. The Compound workflow now also creates its simulator on the iOS 27.0 runtime.

The xcode-27 runners are slower, so give the unit, Enterprise, Compound and snapshot recording jobs longer timeouts.

* Fix the preview snapshot layout on iOS 27

On iOS 27 the hosting controller applies the window's safe area when rendering, but the snapshot size comes from sizeThatFits, which ignores it. Component previews were shifted down and clipped at the bottom. Disable the hosting controller's safe area regions for these snapshots so the measured and rendered layouts match.

swift-snapshot-testing also renders into a window without a scene, and on iOS 27 a navigation stack in such a window reserves space for a large title it never shows, leaving a gap below the navigation bar. Attach the render window to the app's scene until swift-snapshot-testing handles it.

The invite users and add rooms screens sized their selected items row from a measured width stored in state, which is zero on the first layout pass, so snapshots could draw the row empty. Use containerRelativeFrame instead. The add rooms search previews now also wait for their results, and the search previews that show a clear button get a small precision tolerance, as it renders inconsistently in snapshots.

* Re-record the preview snapshots on iOS 27.0

Recorded on an iPhone SE (3rd generation) simulator running iOS 27.0 with Xcode 27.0 (27A266a). The changes come from iOS 27 rendering: text anti-aliasing, hyphenation and line breaking, navigation bars and toggles that now render in snapshots, lazy stacks that were blank on iOS 26, and pseudo-language locale formatting.

* Fix the UI tests on iOS 27

iOS 27 no longer exposes accessibility identifiers on SwiftUI menu items, so the space exploration test now finds them by label within the menu. On iPad, the start chat search field keeps focus after submitting and can swallow the next tap, so retry the tap if the create room screen doesn't appear.

* Re-record the UI test snapshots on iOS 27.0

Recorded on iPhone 17 and iPad (A16) simulators running iOS 27.0 with Xcode 27.0. The changes come from iOS 27 styling, like the keyboard height and glass borders. This also removes the iPad app lock setup snapshots, which have been orphaned since those tests stopped running on iPad.

* Fix the accessibility tests crashing on iOS 27

The accessibility tests' previews wrapper kept its ImageRenderer alive across the awaits that follow the render. On iOS 27, releasing it at that point tears down its view graph outside of a SwiftUI update, which hits an assertion and crashes the app. Release the renderer as soon as the image has been rendered.

* Keep the accessibility tests' image renderers alive on iOS 27

Releasing the ImageRenderer straight away wasn't enough: on iOS 27, releasing one that rendered a preview crashes SwiftUI whichever context it happens in. Keep the renderers alive instead. Each test launches a fresh app, so they only live for the one test.

* Share the UI tests' app settings with their mock user session

Since the user session flow reads its settings from UserSettings, the UI tests' mock user session was wrapping a fresh volatile AppSettings instead of the one the tests configure. The flags that skip onboarding weren't picked up, so every user session UI test got stuck on the notifications permission screen.

* Re-record the Compound snapshots on iOS 27.0

Apply the same safe area fix as the app's preview tests, so size-that-fits snapshots aren't clipped on iOS 27, and re-record the snapshots on an iPhone SE (3rd generation) simulator running iOS 27.0 in English (US).

* Fix a race reading the keyboard in the accessibility tests

On iOS 27 the QR code login screen's keyboard can be dismissing between checking that it exists and reading its frame, which fails the test. Snapshot it in a single query instead.
2026-09-29 17:02:48 +02:00
Element CI 90425460f9 Bump the calendar version ready for the next release 2026-09-29 16:30:46 +02:00
Danny d74d53e4ee Extend the room details screen hook (#6173)
* Allow Enterprise sections in room details

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>

* Update the Enterprise submodule and strings

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>

---------

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>
2026-09-28 12:11:23 -06:00
Stefan Ceriu e51d869fdc Adopt the new send queue interfaces 2026-09-28 15:36:52 +03:00
Copilot 690f2be5f4 Resume app lock continuation after forced logout (#6199)
* Initial plan

* Resume app lock continuation on forced logout

Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com>

* Add regression coverage for forced logout verification

Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>

* Revert "Add regression coverage for forced logout verification"

This reverts commit 64cda7c665ab5ca736110d606250c26e22234619.

Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: pixlwave <6060466+pixlwave@users.noreply.github.com>
Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>
2026-09-28 12:28:01 +02:00
Doug 07b8d1fcb9 Update the Enterprise submodule. 2026-09-28 13:04:37 +03:00
Doug a948e2d3aa Rebase on develop. 2026-09-28 13:04:37 +03:00
Doug c0469e681f Fix unit tests. 2026-09-28 13:04:37 +03:00
Doug fccad5fc30 Replace AppSettings with UserSettings from the UserSessionFlow and below.
Remove some redundant injections when passed alongside a user session too.

# Conflicts:
#	ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift
#	ElementX/Sources/Screens/Timeline/TimelineViewModel.swift
2026-09-28 13:04:37 +03:00
Doug 54cc1cba60 Add a new AccountSettings type and compose it with AppSettings into UserSettings.
The new type doesn't contain any settings yet.
2026-09-28 13:04:37 +03:00
Stefan Ceriu 44fb05f489 Make Sentry and PostHog default settings explicit
... and clearly disable the intrusive ones
2026-09-28 11:04:22 +03:00
Stefan Ceriu 9205e11193 Bump the RustSDK to v26.09.25 2026-09-25 17:16:57 +03:00
Mauro ad1d7a3010 Add multi selection to the Forward action (#6190)
* Start message selection from the Forward action

Behind the multi-select developer option, choosing Forward on a message
now starts a selection with that message checked, instead of the
dedicated Select menu action which is removed. Only forwardable
messages can be selected: remote, decrypted, non-redacted events that
aren't state, unsupported, poll or live location items.

The chrome follows the Figma exploration: the room header stays, an X
in the trailing toolbar position ends the selection, rows show only the
checkbox (no highlight, 10pt gap to the bubble), and the composer is
replaced by a single primary "Forward N messages" button. The limits
are now 10 messages into 5 rooms.

Tapping the button forwards the selection in timeline order, never in
the order the messages were tapped. MessageForwardingItem carries
several contents and the forwarding screen sends them one after the
other per room, reporting a single error toast on partial failures.
The selection is kept while the forwarding screen is open so cancelling
it returns to the selection; it is cleared by the flow coordinator once
the messages are sent, or by the X in the timeline.

New plural action_forward_messages lives in Untranslated for now;
action_select and screen_room_selection_count are no longer used.

* Tidy the selection code after review

Reuse ToolbarButton's cancel role for the selection's toolbar item so it
matches the other screens on every OS version, and disable the room
header while selecting so the room details can't cover the selection.

Move the forwardable/selected item helpers next to the existing
nonisolated array extension, drop the unused MessageForwardingItem
initialisers and the single-item makeForwardingItem overload, and
simplify the forwarding loop now that a room either sends or isn't
reachable.

The action bar preview builds its own view model instead of putting
the shared mock into selection mode for every later preview, and a
test locks that Forward keeps forwarding directly in the pinned
timeline.

* Select messages to forward from the pinned timeline too

Forward is offered in the pinned events timeline, so with the flag on
it starts a selection there as well, with the same cancel item in the
toolbar and the same forward button at the bottom. Forwarding from
that sheet already dismisses the whole sheet, so the selection is
discarded with it. Only the media timelines keep forwarding a single
item directly.

* Confirm with a toast when messages are forwarded to several rooms

When a single room is picked the flow opens it, which is confirmation
enough. When several rooms are picked the user stays in the origin room,
so a "Messages forwarded" toast tells them it worked, as in the designs.
The new screen_room_message_forwarded / screen_room_messages_forwarded
strings live in Untranslated for now.

* Apply review suggestions to multi-message forwarding

Strings
- The forward button and the multi-room toast now use the Localazy
  plurals action_forward_messages and screen_room_messages_forwarded;
  the toast shows the count. The Untranslated copies and the unused
  action_select are gone.

Selection
- Drop the dead startSelection view action and the redundant
  canSelectMessages guard; tests start a selection through Forward
  like the app does.
- Remove two comments on the selection state that restated the code.

Forwarding
- After a send, clear the selection in the room and any open thread
  instead of working out which one started it; only one can be
  selecting at a time.
- Explain that stickers are excluded because the SDK can only forward
  m.room.message content.

* Rename the timeline selection to message selection

"Selection" alone was too generic in a timeline that also has text and
room selections. The selection state, toolbar, action bar and
accessibility types, the timeline state's property, the view actions
and the public clear method now say "message selection". Private
helpers keep their short names.

* Rename MessageForwardingItem to MessageForwardingPayload

The type now carries several messages, so "item" no longer described
it. The factory method and the variables and action labels holding it
follow the new name.
2026-09-25 11:51:14 +00:00
Stefan Ceriu fc8ee9d93d Room list notifications tweaks (#6106)
We decided against changing how unread activity is rendered in the room list, so the experiment and its developer option are no longer needed.
we have also added a notification settings entry for hiding the room list gray dots.
2026-09-25 13:36:33 +03:00
Stefan Ceriu cc07e07833 Fix UI test snapshots
after enabling room list badge counts and changing the rail trailing padding.
2026-09-25 12:13:08 +03:00
Doug 1b12955fa6 Handle unexpected user status emoji values gracefully. 2026-09-24 20:56:25 +03:00
renovate[bot] c2f1dcc024 Update codecov/codecov-action action to v7.1.1 2026-09-24 15:50:34 +03:00
Stefan Ceriu 12d937a5a8 TabRail layout fixes (#6172)
* Attempt another TabRail layout fix by just keeping it visible all the time
* Move one of the sidebar dividers to the tab rail.
2026-09-24 15:47:46 +03:00
Stefan Ceriu 5e56bf3468 Enable the search index store for the authentication client too 2026-09-23 18:37:42 +03:00
Stefan Ceriu df3bdb992a Share the test passphrase through a RestorationTokenFixtures enum.
The legacy restoration token formats store the passphrase as a base64 string
whereas the current one exposes Data, so keep both representations of the
same value in one place instead of repeating the conversions in every test.
2026-09-23 18:37:42 +03:00
Stefan Ceriu b1923b418f Expose the store passphrase as Data instead of a base64 string. 2026-09-23 18:37:42 +03:00
Stefan Ceriu 18201ac248 Switch all clients to the new .highEntropyPassphrase mechanism.
Which will automatically migrate the existing passphrase and allow us to later
switch directly to the raw key everywhere.

More details on https://github.com/matrix-org/matrix-rust-sdk/pull/6878
2026-09-23 18:37:42 +03:00
Stefan Ceriu c8250f6aa4 Tweak the BuildSDK command line tool now that ElementCall also links the SDK 2026-09-23 18:01:39 +03:00
Stefan Ceriu 8572150b70 Remove the room list notification count feature flag.
The room list now always shows the unread notification count for highlighted rooms
and the app icon badge always uses the SDK's own unread notification count, both in
the app and in the NSE. The push payload's unread count is no longer used, so its
UNNotificationContent helper and user info key have been removed along with the
setting, its developer options toggle and the feature-disabled unit test.
2026-09-23 17:57:42 +03:00
Stefan Ceriu 2e7d78ea01 Remove the automatic back pagination feature flag.
Automatic back pagination is now always enabled when building the client, so the
setting and its developer options toggle have been removed.
2026-09-23 17:57:42 +03:00
Stefan Ceriu 8f4a0322b7 Update project and package.resolved following dependency updates and failed CI step 2026-09-23 15:46:25 +03:00
renovate[bot] 5811f38357 Update Project Dependencies (#6185)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-23 12:41:09 +01:00
renovate[bot] f77d4fe04a Update dependency pointfreeco/swift-snapshot-testing to v1.19.5 (#6184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-23 12:32:34 +01:00
renovate[bot] 0462cb4df0 Update dependency swiftlang/swift-syntax to v604 (#6186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-23 12:31:33 +01:00
Doug 7c758df208 Show the call buttons on macOS when the experimental native calling feature is enabled. (#6183)
* Show the call buttons on macOS when the experimental native calling feature is enabled.

* Start native calls in their own window on macOS.

* Update the Enterprise submodule.
2026-09-23 12:24:54 +01:00
Element CI 27ffd4f3d5 Prepare next release 2026-09-23 04:02:24 -07:00
Doug 47f881c507 Only make toolbar buttons prominent when tinted. (#6188) 2026-09-23 11:21:47 +01:00
Doug 550b14718b Update the SDK. (#6182) 2026-09-22 18:01:02 +01:00
Valere Fedronic a236cca24e Bump element-call native to 0.1.0-rc.8 (#6181) 2026-09-22 17:59:41 +01:00
Stefan Ceriu 379a5cd1bc Drop Intel device support from the build scripts and project 2026-09-22 19:56:06 +03:00
Doug 223ddeae22 Fix the split view layout on iOS 27. (#6180) 2026-09-22 17:41:23 +01:00
Doug 8ba47a333a Update the Start Chat button's icon. (#6179) 2026-09-22 17:40:59 +01:00
Doug 375d98e0b5 Fix HDR UI bugs on iOS 27.0. (#6177) 2026-09-22 13:27:08 +01:00
renovate[bot] cfeb38aee9 Update dependency siteline/SwiftUI-Introspect to v27 2026-09-22 09:16:59 +03:00
renovate[bot] a3d72aba1f Update codecov/codecov-action action to v7.1.0 2026-09-22 08:33:05 +03:00
Valere 6b7ac797d4 logs: Add specific prefix for native call logs 2026-09-21 16:49:28 +03:00
Valere 7e68e2cd49 chore: Bump element-call-ios to 0.1.0-rc.7 2026-09-21 15:32:58 +03:00
Element CI 0745a28e57 Translations update 2026-09-21 13:33:01 +03:00
Valere Fedronic fb49f5b83b Add experimental native calls behind a developer flag (#6159)
Add the element-call-ios dependency and a developer option for native calls

The package carries the call UI and the matrix-rust-rtc media stack, pinned by
exact version the way the SDK is, with the local path kept commented out for
working against a checkout. Its media xcframework has no Intel simulator slice
and a package cannot say so for the projects that consume it, hence the
excluded architecture at the project level.

The developer option is off by default and nothing reads it yet. It is placed
next to the Element Call URL override, which only applies to the web view that
native calls replace.
2026-09-17 19:42:18 +03:00
Stefan Ceriu fe18472556 Bump the RustSDK to v26.09.17 2026-09-17 19:34:15 +03:00
Ghazi Tozri 5741290471 Cancel message edit if content remains unchanged 2026-09-17 16:56:14 +03:00
Stefan Ceriu de13fa5438 Fix some warnings 2026-09-16 11:37:11 +03:00
Stefan Ceriu eaa6a5ce25 Dismiss the whole search tab and move back to the previous when when tapping the search field dismissal button. 2026-09-16 11:37:11 +03:00
Stefan Ceriu b8a867bbb4 Dismiss the keyboard on drag 2026-09-16 11:37:11 +03:00
Stefan Ceriu e6cffcba3f Clear the search query when leaving the search tab
Switching away from the search tab now resets the query, so returning to
it shows the recent searches instead of the previous results. The search
field no longer focuses itself on appearance.
2026-09-16 11:37:11 +03:00
Stefan Ceriu 82e666bfe1 Only show joined rooms in the search results
Rooms the user is invited to, or has knocked on, are handled by the chats
list and aren't somewhere search should lead. The search screen now asks
the room summary provider for joined rooms only.
2026-09-16 11:37:11 +03:00
Stefan Ceriu 482cc0fee8 Show recent searches on the search screen
The search screen's empty state now lists the queries the user searched
for and the rooms they opened from the results, newest first, capped at 25
and stored in AppSettings. Tapping a query fills the search field, tapping
a room opens it, and both are reachable with a hardware keyboard.

Rooms are resolved from their identifiers as the room list loads, and
those the user has left or is only invited to are dropped. The history is
cleared on logout.
2026-09-16 11:37:11 +03:00
Stefan Ceriu 9bff070648 Hide the search mode picker until there's a query
The chats/messages picker has nothing to switch between before a query is
entered, so it only shows once the user starts searching.
2026-09-16 11:37:11 +03:00
Stefan Ceriu af202f3466 Add a joined only option to the room list search filter
The search filter now takes a joinedOnly flag, which appends the SDK's
joined filter so that invited and knocked rooms are excluded. It defaults
to false, leaving the existing callers unchanged.
2026-09-16 11:37:11 +03:00
renovate[bot] 942535e4fa Update zizmorcore/zizmor-action action to v0.6.4 2026-09-16 10:22:35 +03:00
Doug 64aae09c6d Add an AccountProvider type and use it within the authentication flow. (#6103)
* Add an AccountProvider type and use it within AppSettings.

* Use the AccountProvider type within the Authentication flow too.

* Fix unit test compilation with Swift 6.4

* Make sure defaultAccountProvider takes allowOtherAccountProviders into account.

* Document a caveat on using AuthenticationService.homeserver.value.accountProvider.
2026-09-14 15:25:33 +01:00
ElementRobot 26fddb8aa2 Translations update (#6151)
Co-authored-by: Element CI <ci@element.io>
2026-09-14 08:44:14 +01:00
Doug 5d050f6dfc Fix some SwiftLint warnings. 2026-09-11 08:56:37 +03:00
renovate[bot] 268ed62af9 Update Project Dependencies (#6136)
* Update Project Dependencies

* Run XcodeGen, update Package.resolved and Settings.bundle

* Run XcodeGen, update Package.resolved and Settings.bundle

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Element CI <ci@element.io>
2026-09-10 16:23:40 +01:00
Doug 4608a63e29 Fix some isolation errors when compiling with Swift 6.4. (#6148)
* Fix some isolation errors when compiling with Swift 6.4.

* Update the Design Tokens dependency to include Sendable conformances.
2026-09-10 15:22:53 +01:00
Doug a150df584d Update the read marker colour to match the Figma. (#6146) 2026-09-10 13:26:10 +00:00
Stefan Ceriu 58628bd7c5 Introduce a local mechanism for managing the app's badge count 2026-09-10 13:03:10 +03:00
Element CI 7be93f8da8 Prepare next release 2026-09-10 02:19:03 -07:00
Mauro e57d872fc8 Scope dark mode change only to the presenting sheet (#6144)
* Scope dark mode change only to the presenting sheet, instead of the whole window

* Apply review suggestions to the presentation colour scheme modifier

API:
- Take a `ColorScheme?` at the SwiftUI level instead of a `UIUserInterfaceStyle`
  and map it once inside the representable (`nil` means no override).
- Rename to `presentationColorScheme` and the file to match.
- Restore the `preferredColorScheme` property on the media preview details and
  redact confirmation views, and the `nil` at the media timeline call site, so
  no UIKit types leak into the views.

Cleanup:
- Walk the responder chain with `sequence(first:next:)` instead of a while loop.

* Re-record the media upload preview iPad snapshots

The Cancel button no longer has an explicit tint, so its label colour changed
very slightly from `textActionPrimary` to the default. Only the iPad references
include the toolbar, which is why the iPhone ones were unaffected.
2026-09-10 10:23:56 +02:00
Doug 9fe64b7cd4 Update the SDK. 2026-09-10 08:27:08 +03:00
Doug b652dfe69a Add a couple of points about code style/ordering to the AGENTS.md (#6133) 2026-09-09 15:47:15 +01:00
Doug 8401c2612a Limit the custom status input to 30 characters. (#6139) 2026-09-09 13:14:12 +01:00
Mauro 426fdc70a1 Add a developer option and selection mode for timeline messages (#6102)
* Add a developer option and selection mode for timeline messages

First step of the multi-select messages feature (element-meta#3255, iOS
counterpart of element-x-android#6918). Everything is gated behind the new
`messageMultiSelectEnabled` developer option and nothing changes while it
is off.

- Adds a "Select" action to the timeline item menu for remote messages in
  the room, focused and thread timelines. Choosing it enters selection
  mode with that message selected.
- While selecting, every bubbled row shows a checkbox on its leading edge
  and tapping or long pressing a row toggles it. State events, redacted
  messages, live locations, unsupported events and local echoes cannot be
  selected and stay inert. Selected rows get the accent background.
- The navigation bar swaps to a Cancel button and an "N selected" title,
  the back button is hidden and the composer is removed until the
  selection ends (Cancel or deselecting the last item). This is the hook
  where the bulk action bar will land in the next PR.
- The selection is capped at 30 messages, showing a toast when the cap
  is hit, and is cleared if the developer option is switched off.
- The selection state lives in `TimelineViewModel` so room and thread
  timelines share the behaviour without extra plumbing.

Includes unit tests for the menu action gating and the selection state
transitions, plus RoomScreen and bubble styler previews for the new state.

* Harden the timeline selection mode after review

- Reconcile the selection with timeline updates: selected messages that
  become unselectable (e.g. redacted) are dropped, and the selection is
  cleared when the timeline is swapped for a focused or live one, so the
  count can never refer to items that aren't on screen.
- Keep the composer mounted but collapsed while selecting, instead of
  removing it from the hierarchy. This preserves the draft and composer
  mode and avoids the disappear side effects. The footer with the identity
  warnings stays visible.
- Give the long press modifier an `isEnabled` flag so the recogniser is
  disabled while selecting, rather than only skipping the action while
  still animating and buzzing.
- Gate the Mac context menu on the selection like the other gestures.
- VoiceOver: a selectable bubble becomes a button carrying the selected
  trait whose default action toggles it, non-selectable rows lose their
  message actions while selecting, and reactions, thread summaries and the
  checkbox are hidden from the accessibility tree in selection mode.

* Stop voice message recordings when entering selection mode

The composer is collapsed rather than removed while selecting, so an
in-progress recording would otherwise keep the microphone open out of
sight. Entering the selection now stops the recording, which moves the
composer to the preview state so the message is still there to play or
send once the selection ends. Playback of voice messages is unaffected.

* Use the translated strings for the message selection mode

The selection strings have been imported to Localazy, so the untranslated
placeholders are dropped in favour of `action_select`,
`screen_room_maximum_messages_selected` and the `screen_room_selection_count`
plural. Only these keys are taken from the download to keep the change
focused.

* Apply review suggestions to the message selection mode

Addresses the first review round on the selection mode:

- Undecryptable messages are no longer selectable, as none of the bulk
  actions can be taken on them from the single item menu either.
- Renames for clarity: the menu action is `selectMessages` (leaving room
  for a future `selectText`), the view action is `startSelection`, the
  state exposes `contains(_:)`, `limit` and `isAtLimit`, and the composer
  collapsing modifier is `collapsedInPlace(_:)`.
- `canSelectMessages` uses a switch expression, the selection state doc
  comment refers to a feature flag rather than Labs, the recording helper
  doc describes only what it does, and `toggleSelection()` moves below
  the subviews.

* Keep the accessibility labels of timeline items while selecting

The selection mode used to replace each item's accessibility body with a
plain combined element, dropping the sender name representation and the
gallery label. The per item labelling now always applies, and the selection
only changes the interaction on top of it: the message actions are offered
as before, or the item becomes a button carrying the selected trait whose
default action toggles it. Polls and galleries collapse into a single
element only while selecting, as their children are inert then.

* Keep the timeline types within the lint limits after rebasing

The redaction reason work on develop grew the menu action handler and the
timeline view model right up to SwiftLint's size limits, so the selection
additions tipped them over. The `selectMessages` case now shares the
`downloadMedia` break and the selection flag subscription lives in the
selection extension.

* Fix the snapshot regressions from the selection mode

- Drop `geometryGroup()` from the bubble row: it stopped the avatar
  placeholder initial from rendering, which showed up as a mismatch in
  every timeline item snapshot.
- Collapse the composer with opacity instead of clipping, as the clip also
  removed the composer background from the home indicator area while the
  composer was fully visible.
- Keep the timeline item menu preview on the default flag-off state so the
  existing menu snapshots stay valid.
- Record the new "Selecting" room screen and "Selection" bubble snapshots
  on the same iPhone SE simulator CI uses.
2026-09-09 13:36:57 +02:00
Stefan Ceriu 0454916b65 Fix start chat UI test snapshots following copy changes 2026-09-09 11:23:12 +03:00
Stefan Ceriu ac183daff8 Show message counts on room list notification badges 2026-09-08 18:09:25 +03:00
Doug 327ca090c5 Fix CHANGES.md entry. 2026-09-08 15:42:00 +01:00
Element CI ef15fd0666 Prepare next release 2026-09-08 06:05:14 -07:00
Doug 0cff8bd98e Update the SDK. (#6134)
Update the SDK handling API breaks.
2026-09-08 13:48:52 +01:00
ElementRobot e32e1dd388 Translations update (#6111)
* Translations update

* update preview tests

---------

Co-authored-by: Element CI <ci@element.io>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-09-08 13:08:25 +02:00
Mauro Romito 098edab1b4 use disambiguated display name for avatar change events 2026-09-08 12:39:07 +02:00
Doug 15627e4512 Minor quicklook fixes. (#6112)
* Only show a spinner when opening the media viewer is slow

* Keep a sent local echo's page in the media viewer

* Fix the caption visibility.

---------

Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
Co-authored-by: Matthew Hodgson <matthew@matrix.org>
2026-09-08 09:40:43 +01:00
Doug 1af16e92db Update the Enterprise submodule. (#6114) 2026-09-07 19:00:16 +01:00
Mauro 346a1e8ebb Redaction reason (#6098) 2026-09-07 18:57:05 +02:00
Stefan Ceriu 350d47c94a Bump the RustSDK to v26.09.07 2026-09-07 15:34:44 +03:00
renovate[bot] c5675eeb9b Update zizmorcore/zizmor-action action to v0.6.3 2026-09-07 09:51:31 +03:00
Stefan Ceriu f224c104e0 Fix the tab rail flickering on narrow iPad windows (#6107)
* Fix the tab rail flickering on narrow iPad windows

The rail is laid out beside the split view, so hiding it when the split
collapsed handed that width back to the split, which then had room for its
sidebar again, which showed the rail, which collapsed the split. Any window
whose width sat within a rail of the split's collapse threshold flickered
forever, an iPad mini in fullscreen among them.

The split reports the same column visibility whether it collapsed on its own or
because the sidebar was toggled, and at the same width, so the two can only be
told apart by when they arrive: the rebound lands in the next layout pass. A
collapse that soon after showing the rail is therefore taken as proof that the
rail and the sidebar don't fit at this width, and the rail is left hidden until
the window is resized. Later collapses are treated as deliberate and still hide
the rail as before.

The container width, the width the rail and the sidebar were shown not to fit
at, and the moment the rail was last shown are one piece of state with
invariants between them rather than three independent values, so keep them and
the decision they feed together in TabRailVisibility.
2026-09-04 17:46:03 +03:00
renovate[bot] b4395d948f Update dependency siteline/SwiftUI-Introspect to v26.0.2 (#6104)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-09-04 08:58:42 +01:00
Stefan Ceriu 4355307ba0 Add support for <details> and <summary> tags (#6077) 2026-09-03 11:47:12 +03:00
Stefan Ceriu 7052751e38 Update preview test snapshots 2026-09-03 11:44:32 +03:00
Stefan Ceriu 491f7c538e Merge the room thread list flag into the threads flag 2026-09-03 11:44:32 +03:00
Stefan Ceriu 2ae8b9bf75 Promote knocking to labs 2026-09-03 11:44:32 +03:00
Stefan Ceriu 6839258111 Promote gallery messages to labs 2026-09-03 11:44:32 +03:00
Stefan Ceriu 8752897bc3 Fix UI test snapshots following copy changes and filter reordering 2026-09-03 10:36:31 +03:00
Ghazi Tozri fadd19d847 Fix photo picker selection tint on iOS 26
On iOS 26 the picker's remote process samples the window's tint color when it connects, ignoring the view-level override, which left the selection badges white on grey in dark mode. Temporarily override the window tint while the picker is presented and restore it when picking finishes or the picker is dismantled.
2026-09-02 15:28:43 +02:00
Matthew Hodgson eec2facab1 Add a Mentions room list filter (#5885)
* Add a Mentions room list filter

Adds a new filter chip to the room list that shows only rooms with
unread mentions, using the new mentions room list filter from the
Rust SDK (matrix-org/matrix-rust-sdk branch matthew/mentions-filter).

The SDK version needs bumping to a release that includes the new
filter kind before this can build against the released components.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Order the room list filters by expected usage

Move Favourites ahead of People and Rooms so the more commonly used
filters (Unreads, Mentions, Favourites) sit first in the chip row —
Rooms and Invites are used rarely enough that it's fine for them to
wrap off the side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uk8aPxHn3BHCe52L226jdG

* update the SDK and use the appropriate rust filter types

* preview tests + app settings flag for mentions

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-09-01 18:08:00 +02:00
Mauro 86bc4d54b6 Autoplay the next voice message if it is the next timeline item. (#6089) 2026-09-01 18:00:14 +02:00
Ghazi Tozri b4a36ad465 hide the forward action on live location shares (#6091)
Live location shares have no message event content, so forwarding always failed silently. Exclude them from `isForwardable` matching polls logic
2026-09-01 16:23:20 +03:00
Element CI 53df7dd24d Bump the calendar version ready for the next release 2026-09-01 10:15:39 +02:00
Sergey Ospanov aa4e116e01 Ignore incompatible room filter activations
Signed-off-by: Sergey Ospanov <33452977+sergeyospanov@users.noreply.github.com>
2026-08-31 13:36:51 +02:00
ElementRobot 9b22ca3cd2 Translations update (#6088)
* Translations update

* swiftformat .

* update previews

---------

Co-authored-by: Element CI <ci@element.io>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-08-31 12:30:26 +02:00
Stefan Ceriu 32f86da98d Replace danger-swift with a actions/github-script version (#6080)
* Replace danger-swift with a actions/github-script version
* Report PR check results in a comment

Annotations are only visible in the Checks tab, so the results were easy to
miss on the pull request itself. The workflow now keeps a single comment up to
date on the Conversation tab, listing failures and warnings together, and
deletes it again once there is nothing left to report.

Commenting on pull requests from forks needs a token with write access, which
the pull_request trigger does not provide, hence the switch to
pull_request_target. The job never checks out or executes any code from the
pull request, it only reads the event payload and the list of changed files.
2026-08-27 17:50:02 +03:00
Mauro 9f7e30820b Use server name from user id function (#6082)
update sdk and use the new server from user id function from the sdk
2026-08-27 16:28:40 +02:00
Stefan Ceriu efefd549a2 Limit link previews width in the timeline to 300 points. 2026-08-27 16:45:41 +03:00
Aaron Thornburgh 600b9224b3 Updates to Notification Settings (#6075)
* Updates to Notification Settings

Move "Invitations" and "Notify me on @room" to the section labeled "Notify me for"

* Record preview snapshots

* Update @room mentions label.
2026-08-27 14:21:15 +01:00
Doug 2b9ec0336d Try using Danger with the Node/Docker fix? (#6079)
* Try using Danger with the Node/Docker fix?

* Allow danger to post to PRs again.
2026-08-27 13:06:28 +01:00
Doug b6a7188fb5 Pin project dependencies to exact versions given we have Renovate. (#6078) 2026-08-27 12:39:48 +01:00
Doug 6292ef6bfe Don't focus the user status text field during tests. (#6076) 2026-08-27 11:43:05 +01:00
Mauro 8874ee112c Allow copying blocked user id (#6071) 2026-08-26 14:06:11 +02:00
Mauro Romito 142529ad69 Revert "allow copying blocked user id"
This reverts commit f6f6538bd6.
2026-08-26 13:44:15 +02:00
Mauro Romito f6f6538bd6 allow copying blocked user id 2026-08-26 13:43:14 +02:00
Element CI a139257967 Run XcodeGen, update Package.resolved and Settings.bundle 2026-08-26 10:49:48 +02:00
Element CI ca1d44a53d Run XcodeGen, update Package.resolved and Settings.bundle 2026-08-26 10:49:48 +02:00
renovate[bot] fd72779f00 Update Project Dependencies 2026-08-26 10:49:48 +02:00
Doug fbb5632896 Add some tests.
# Conflicts:
#	ElementX.xcodeproj/project.pbxproj
2026-08-26 08:17:00 +03:00
Matthew Hodgson c812b723dc Advance the join room screen when the server joins us to the room
The homeserver can join a user without a join request from their client,
auto-accepting a knock (element-hq/synapse#16307) or an invite
accepted on another device. The join room screen only navigated on a
user-initiated join and only observed membership changes while knocked,
so a server-side join left it sitting on a stale invited state.
2026-08-26 08:17:00 +03:00
Doug c8b12d9496 Add tests for the UserSessionStore. (#6065)
* Add enough mock configurations to test the UserSessionStore.

* Add tests for the UserSessionStore.
2026-08-25 16:41:43 +01:00
Doug 353233672a Fix settings screen snapshots. 2026-08-25 16:40:21 +01:00
Doug 59ef54974d Update the Enterprise submodule. 2026-08-25 16:40:21 +01:00
Doug 9c65d36101 Use the ClientFactory pattern everywhere replacing raw ClientBuilders.
# Conflicts:
#	ElementX/Sources/Services/UserSession/UserSessionStore.swift
#	NSE/Sources/NSEUserSession.swift
2026-08-25 16:40:21 +01:00
Element CI 5481f35727 Prepare next release 2026-08-25 06:21:40 -07:00
Mauro Romito 7ecb7f89e6 removed no op line for client builder 2026-08-25 15:04:10 +02:00
Mauro Romito d611bad03b update sdk to 26.08.25 2026-08-25 15:04:10 +02:00
Stefan Ceriu 328efce428 A11y tests: prevent failures caused by a visible keyboard 2026-08-25 15:11:22 +03:00
Stefan Ceriu 313f250273 Fix testAcceptSpaceInvite snapshot 2026-08-25 15:11:22 +03:00
Stefan Ceriu ae4620d7d6 Fix the SettingsScreenUserStatusRow by ignoring the keyboard's empty container 2026-08-25 15:11:22 +03:00
Doug ec105aadf5 Update the Enterprise submodule. (#6063) 2026-08-25 11:39:57 +01:00
Mauro Romito 024b02324e update preview tests 2026-08-24 14:49:17 +02:00
Element CI b864918686 Translations update 2026-08-24 14:49:17 +02:00
Stefan Ceriu 925e61e40c Preserve the blank lines typed after a list
The newline to break replacement only matched paragraph to paragraph, so a blank
line after a list was dropped and the text following it glued to the last item.
Only a following paragraph is matched, because without a blank line its text
would have continued the last item instead of starting a paragraph of its own.
2026-08-24 15:45:19 +03:00
Stefan Ceriu 8ead25eaa3 Stop the integration tests from backgrounding the web authentication session
Tapping the SafariViewService app object makes XCUITest activate it as a
standalone app. That backgrounds Element X, which hosts the web view, so the
hosted scene goes to the background too and WebKit suspends it mid load. The
sign in page is fetched successfully but never rendered, and the test times
out waiting for the username field.

The tap dates from when the element was a web view inside the app, where it
was a plain in app tap, so it no longer does what its comment describes.
2026-08-24 15:18:48 +03:00
Doug 84556c0672 Enable the User Status feature on supported homeservers. (#6059)
* Remove the user status feature flag.

* Handle User Status in tests.
2026-08-24 11:00:31 +01:00
Stefan Ceriu 1ba090b914 Fix missing back pagination attempt
Avoiding not populating past the initial events
2026-08-21 17:38:49 +03:00
Stefan Ceriu c65147678a Add support for the <strike> tag in the attributed string builder 2026-08-21 17:01:54 +03:00
Stefan Ceriu 70714dd7f7 Update preview test snapshots 2026-08-21 17:01:54 +03:00
Stefan Ceriu d360d59b0b Fix indentation for various nested nodes and in particular lists
Fixes #5179. Best see the updated preview snapshots
2026-08-21 17:01:54 +03:00
Doug f454dd593c Update the Enterprise submodule (#6053) 2026-08-21 12:42:49 +01:00
Doug 6945329581 Update the command line tools and fix the Enterprise dependency. 2026-08-21 11:33:44 +01:00
Doug 0d222e8ac0 More tweaks for User Status. (#6051)
* Make sure user profiles are up to date when mentioning.

* Automatically focus the custom status text field.

* Make sure to clear the previous custom status text before input.

* Attempt to fix the user status sheet sometimes disappearing.
2026-08-21 11:23:33 +01:00
renovate[bot] dd84da15b0 Update dependency element-hq/compound-design-tokens to v10.2.4 2026-08-21 12:28:41 +03:00
Stefan Ceriu 8a3468658b Address PR comments 2026-08-21 12:26:02 +03:00
Stefan Ceriu 72d55e485a Hopefully fix the flakey unignoreFailure RoomDetailsScreenViewModelTests 2026-08-21 12:26:02 +03:00
Stefan Ceriu 0bd571d068 Relaunch the test process on retrying after failures.
So that runner exits continue the retry flows.
2026-08-21 12:26:02 +03:00
Stefan Ceriu 0eea289b89 Fix test fixture copy paste error 2026-08-21 12:26:02 +03:00
Stefan Ceriu 9dd9fd9176 Prefetch the next room list page before reaching the bottom
Growing the list only once the last row was already visible, on top of the half a
second visible range throttle, meant hitting and bouncing off the end of the list
while the next page loaded. Ask for the next page half a page early instead, so
the growth happens while the user is still scrolling towards the boundary.

The threshold is clamped so that it stays positive for accounts with fewer rooms
than half a page, where it would otherwise always match and leave the list unable
to shrink back to a single page.
2026-08-21 12:26:02 +03:00
Stefan Ceriu e7787f4c3f Set the room list mode synchronously on a warm launch
The room summary provider subscription only delivers its current state through an
async main queue hop, which can end up queued behind the first render. On a warm
launch, where the cached rooms have already been published before the home screen
exists, that leaves the skeletons up for the whole duration.

Read the current state at the end of init instead, alongside the rooms which were
already being read there.
2026-08-21 12:26:02 +03:00
Stefan Ceriu 8c9edd5ea9 Fix the room list flashing empty on launch
The room summary provider publishes its loading state and its rooms through two
independent streams, so the state can report loaded before the first summaries
have been applied. Flipping the room list mode to .rooms at that point renders
an empty list until the summaries arrive.

Combine the two publishers into a single subscription so that the mode and the
rooms are always updated from the same pair of values, and keep the skeletons up
until the rooms have published. Only the initial transition is held back, an
empty list later on is a filter or a search result and must not send the mode
back to the skeletons.
2026-08-21 12:26:02 +03:00
Stefan Ceriu bb0e80ba16 Remove unnecessary updateBackupStateFromRemote call
This is a remote check fired on every startup and the `backupStateListenerTaskHandle`'s
unknown state already handles it.
2026-08-21 12:26:02 +03:00
Doug 7edafe6b35 Update the command line tools. 2026-08-21 10:06:19 +01:00
Element CI a90ca1ff22 Prepare next release 2026-08-20 02:20:15 -07:00
Doug 2a808f9b58 Update the Enterprise submodule. (#6049) 2026-08-20 10:04:48 +01:00
Stefan Ceriu cd453bc6fa Move xcresultparser to the Tools to avoid long recompilation times through brew 2026-08-19 07:41:26 +03:00
Stefan Ceriu 771d10ac6b Cache CI test run compilations 2026-08-19 07:41:26 +03:00
Stefan Ceriu 5f49c25952 Fix wrong media duration assertions and invalid tolerances 2026-08-19 07:41:26 +03:00
Stefan Ceriu 804fccf650 Reduce the size of the portrait test video 2026-08-19 07:41:26 +03:00
Doug 864d022f45 Allow the Chats title to shrink when filtering on a space with a long name. 2026-08-18 17:59:06 +01:00
Doug b27a549ef8 Update the toolbars in the Chats/Spaces screens. (#6043)
* Fix an issue with macro compilation when using Xcode previews.

* Update the Chats/Spaces toolbars to match the Figma.

Fixes a bug with the status emoji in the liquid glass button.
2026-08-18 17:50:15 +01:00
Stefan Ceriu bf2561f937 Delay secondary room summary provider setup (#6038)
To avoid creating room summaries for them on startup, especially as the static one uses a page size as big as the whole account.
2026-08-18 12:52:47 +03:00
Stefan Ceriu fa2b13864a Use normal path for error toast display 2026-08-18 11:55:32 +03:00
Stefan Ceriu 7ce16c5b2d Rework accessibility identifier comment 2026-08-18 11:55:32 +03:00
Stefan Ceriu f3c8438bd8 Add send alert tests 2026-08-18 11:55:32 +03:00
Stefan Ceriu 64076c6cf9 Show the failure alert even when no send handle is available 2026-08-18 11:55:32 +03:00
Stefan Ceriu 28966c8397 Update PostHog 2026-08-18 11:55:32 +03:00
Matthew Hodgson 8bc7e7c281 Address review feedback on the send-failure dialog
- Resolve the send handle at tap time and pass it through the alert,
  removing the duplicated lookup and the stale-itemID window between
  tapping the indicator and pressing Retry.
- Route 'Remove message' through the existing timeline item menu redact
  action instead of a bespoke redact path with a fatalError.
- Surface a retry failure to the user with an indicator toast rather
  than only logging it.
- Only set the send-info accessibility identifier when the label has a
  tappable status, so plain timestamps don't create ambiguous matches.
2026-08-18 11:55:32 +03:00
Matthew Hodgson 404410b8a2 Add an accessibility identifier to the timeline item send info
Lets UI tests (and accessibility tooling) target the tappable send-info
label (timestamp / Sending failed) directly.
2026-08-18 11:55:32 +03:00
Matthew Hodgson 1aa9b29eb5 Offer Retry and Remove actions with a reason when tapping a failed message
Tapping the red (!) on a message that failed to send used to show a bare
"Sending failed" alert with only an OK button. Now the alert includes the
failure reason from the SDK (e.g. the HTTP error that caused it) and offers
Retry sending, Remove message and Cancel actions, wired to the send handle's
resend and to redacting the local echo respectively.
2026-08-18 11:55:32 +03:00
Stefan Ceriu 033cb2e038 Fail fast on the integration tests when encountering the first failed assert 2026-08-18 10:41:32 +03:00
Doug d3b068cbad Fix the emoji picker's title when setting a custom status. (#6039) 2026-08-18 08:12:56 +01:00
ElementRobot 55a1a21f08 Re-run XcodeGen. (#6040) 2026-08-18 08:12:28 +01:00
Stefan Ceriu 360d6d0ee1 Attempt another kind of fix for the encryption settings UI test 2026-08-18 08:27:57 +03:00
ElementRobot f090f30e7e Translations update (#6036)
* Translations update

* Update snapshots.
2026-08-17 14:17:05 +00:00
Doug 224edafabd Update the SDK fixing some user status bugs. (#6037)
- Room heroes weren't being updated when their status changed.
- A user's own profile would go missing after removing their avatar (or setting any field to null).
2026-08-17 14:17:11 +01:00
Doug 809965f693 Update the SDK. (#6030)
Use the new Profiles sync extension check for more reactive user profiles.
2026-08-14 16:34:08 +01:00
Stefan Ceriu 5f236c577f Implement TimelineItemProvider diff truncate mechanism 2026-08-14 18:30:26 +03:00
Stefan Ceriu f7f55024ab Fix the flaky ElementCallServiceTests by replacing TestClock
The suite intermittently hung on CI, most recently for the full 2 minute time
limit on lifetimeIsCapped(). A spindump from a failed run shows the process
suspended rather than spinning (0.247s of CPU over 190s) with a background
priority worker thread sitting runnable for roughly 145 seconds without ever
being scheduled.

That band is exactly what swift-clocks' TestClock depends on to make progress:
both advance() and checkSuspension() await Task.megaYield(), which serially
awaits 20 detached tasks at background priority. On an oversubscribed runner
those never run, so the test waits forever instead of failing. The same
mechanism, less starved, is why two of the passing tests took 1.0s and 0.77s
for what is otherwise pure logic. checkSuspension() made it worse, since
asking whether a timer was armed cost a megaYield per poll.

ManualClock replaces it. It holds a single pending sleep, since that is all a
test needs to step through: the sleep registers a continuation, advance(by:)
resumes it synchronously once its deadline passes, and waitForScheduledSleep()
suspends until the timer is armed instead of polling for it. Nothing in the
path touches background priority and no loop's iteration count depends on the
scheduler. The suite now runs in 0.019s, and under a locally saturated CPU the
old version took 1.005s against the new one's 0.037s.

Also drops the now unused Clocks package, removes the 2 minute time limit that
was papering over the starvation, re-enables callIsTimingOut() which the same
flakiness had disabled, and has lifetimeIsCapped() assert the 90 second cap
directly rather than inferring it from when the timer fires.
2026-08-14 16:16:53 +03:00
Doug 1db7a9b07c Update the Enterprise submodule. (#6023) 2026-08-14 13:52:53 +01:00
Stefan Ceriu c62aed4e0c Detach the Client's cacheAccountURL call
So it doesn't block service state transitions while hitting the network.
2026-08-13 18:16:07 +03:00
Stefan Ceriu df6b7d0d21 Avoid re-applying identical room list filters
The home screen's filter observation fires on every view state change, so setFilter was repeatedly pushing the same filter down into the SDK. Cache the last applied filter and skip the SDK call when it hasn't changed.
2026-08-13 17:24:11 +03:00
Stefan Ceriu 5cb4a2fe5a Add CustomStringConvertible conformances to timeline item contents 2026-08-13 16:25:18 +03:00
Stefan Ceriu b41d801cab Prevent invalid composer mode crashes
Cause by voice recording racing against the send action.
2026-08-13 16:25:18 +03:00
Stefan Ceriu d4d74dd8f8 Avoid race conditions between the home screen's crash alert and Sentry's onLastRunStatusDetermined
By converting the `lastCrashEventID` to a publisher that the VM can just listen to instead of relying on start to match a pre-set event
2026-08-13 16:17:21 +03:00
Element CI 9f648803f9 Run XcodeGen, update Package.resolved and Settings.bundle 2026-08-13 09:47:03 +03:00
renovate[bot] b6239bd53c Update dependency PostHog to v3.69.1 2026-08-13 09:47:03 +03:00
Stefan Ceriu 99f03221c9 Sort the new component paths better. 2026-08-12 16:52:26 +03:00
Stefan Ceriu 78563ef09e Update the enterprise submodule 2026-08-12 16:52:26 +03:00
Stefan Ceriu c0e19454e3 Move BuildExtensions into the Components folder
Complete grouping the standalone modules under Components. Updates the local
Swift package path in project.yml.
2026-08-12 16:52:26 +03:00
Stefan Ceriu d595e4fd83 Move MapLibre into the Components folder
Continue grouping the standalone modules under Components. The MapLibre
frameworks were nested inside the app folder even though they are separate
targets, so move them alongside the other components and update the
XcodeGen includes.
2026-08-12 16:52:26 +03:00
Stefan Ceriu f73285db3e Move DevelopmentAssets into the Components folder
Continue grouping the standalone modules under Components. Updates the
preview asset paths and the media resource references in the app and unit
test targets.
2026-08-12 16:52:26 +03:00
Stefan Ceriu cbe17409bb Move Secrets into the Components folder
Continue grouping the standalone modules under Components. Updates the
Secrets.swift references in the app, NSE and Share Extension targets along
with the SwiftLint, SwiftFormat, gitignore, post-checkout hook and forking
documentation paths.
2026-08-12 16:52:26 +03:00
Stefan Ceriu e302b37b6a Move Variants into the Components folder
Continue grouping the standalone modules under Components. Updates the
nightly.yml include path and the app icon banner path used by the
configure-nightly CI command.
2026-08-12 16:52:26 +03:00
Stefan Ceriu 5866c4340a Move SDKMocks into the Components folder
Group the standalone modules that live outside of the app targets under a
single Components folder. Updates the XcodeGen include, the Codecov ignore
path and the Sourcery output path used by the generate-sdk-mocks tool.
2026-08-12 16:52:26 +03:00
renovate[bot] 9c0d17c79c Update Project Dependencies (#6011)
* Update Project Dependencies

* Run XcodeGen, update Package.resolved and Settings.bundle

* Run XcodeGen, update Package.resolved and Settings.bundle

* Opt out of PostHog's ridiculous new notification tracking.

Shouldn't be necessary given we have swizzling disabled, but just to be safe.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Element CI <ci@element.io>
Co-authored-by: Doug <douglase@element.io>
2026-08-12 13:29:38 +01:00
Stefan Ceriu 37c78bf6b1 Add a test covering the map framework being loadable
The shim is embedded but not linked, so nothing at build time proves it is
still there. If it stops being copied into the bundle, or the class is renamed,
dlopen simply fails at runtime and the user gets a blank map behind an error
alert. Nothing in CI would notice.

The test asserts the framework loads and the shim resolves, which is exactly
the pair that breaks if either side of the arrangement is disturbed.
2026-08-12 15:04:24 +03:00
Stefan Ceriu 4de42be688 Load the map framework on demand instead of at launch
MapLibre ships as a dynamic framework, so being named in the app's load
commands means dyld loads it before main runs on every launch: 7.3 MB paged in,
133 C++ static initialisers run and its classes registered, all for a map that
only appears once a location screen is opened.

Measured on an iPhone 17 Pro with two otherwise identical probe apps launched
alternately, linking MapLibre takes pre-main from 11.1 ms to 23.2 ms, and on a
cold launch from 32.9 ms to 60.7 ms. Loading it on demand brings pre-main back
to the 11.1 ms baseline and moves the roughly 11 ms it costs to dlopen to the
moment the map first appears.

MapLibreShim is now embedded but not linked, and the app resolves the shim
through the ObjC runtime after dlopen-ing it. MapLibre stays in the bundle as
an embed-only framework reference so the shim's load command can resolve
against it, expressed as an implicit framework because xcodegen can't describe
embed-without-link for a package product.

Timeline location messages already use the static tile view, which never
touched MapLibre, so the shim only loads for interactive maps.
2026-08-12 15:04:24 +03:00
Stefan Ceriu 8753fe3980 Move the interactive map into a new MapLibreShim framework
Second step towards loading MapLibre on demand: everything that touches it now
lives in one framework, so that framework can later be left unlinked.

The interactive map view, its annotation view and the coordinate animator move
into MapLibreShim, which reaches the app through MapLibreShimProtocol. The app
side keeps its MapLibreMapView call site and becomes a thin wrapper that builds
a MapViewConfiguration, passing the app-only pieces (map style URLs, marker
views, tint colour) across as closures so the shim needs nothing from the app.

LocationAnnotation loses its MapLibre conformance and becomes a plain struct,
with the shim mapping it onto its own annotation type. MapLibre's log
forwarding moves into the shim as well, since TargetConfiguration can no longer
reach it.
2026-08-12 15:04:24 +03:00
Stefan Ceriu 5724e65d8e Move the shared map types into a new MapLibreInterface framework
Groundwork for loading MapLibre on demand. The map view is about to move into
its own framework, and both sides need to agree on the types that cross the
boundary, so ShowUserLocationMode and MapLibreError move out of the app into a
small framework that the app links.
2026-08-12 15:04:24 +03:00
Stefan Ceriu 1febb3ef91 Retry failed XCUITests 2026-08-11 19:51:33 +03:00
Element CI 10867d1246 Prepare next release 2026-08-11 06:32:01 -07:00
Doug 0b0b8a96ff Update the SDK. (#6010) 2026-08-11 12:46:15 +00:00
Doug 947a47943b Set Element Call's user-agent back to the default but include the bundle name.
Apparently livekit-js-sdk does feature detection based on UA sniffing.
2026-08-11 13:11:26 +01:00
Stefan Ceriu d3b12561b3 Fix XCUITest text entry on all paths 2026-08-11 14:42:48 +03:00
Doug 2228da3eb9 Make sure the Tab Rail's background matches the split view's detail background. (#6003) 2026-08-11 11:07:24 +01:00
Stefan Ceriu d57c376b21 Reinforce text typing within XCUITests 2026-08-11 12:52:00 +03:00
Stefan Ceriu 3f6347b6c9 Drop the addressSanitizer and undefinedBehaviorSanitizer from the Accessibility and UI tests
They cost a lot of extra memory to enable and cause problems on constrained CIs
2026-08-11 11:58:18 +03:00
ElementRobot 189a04b94b Run XcodeGen (#6006) 2026-08-11 08:43:16 +01:00
Michael 6efa8743ff Combine the Server Selection screen into the Server Confirmation screen and add autocompletion when entering an account provider. (#5949)
* Adjust login flow to exclusively use the ServerSelectionScreen

* Move async code to server selection vm

* ServerSelection supports pro picker options

* Use homeserver address for both confirmation and picker modes

* rename ServerSelectionScreenMode

* remove ServerConfirmationScreen

It's been superseded by the ServerSelectionScreen

* rename userInput

* Update strings with latest design strings

* Add server input autocompletion

* Store previous servers in AppSettings and autofill on server signin

* Audit changes for logging

* Enforce lowercase server history storage

* Update tests

* Update Xcode project

* PR review fixups

* Add HomeserverHistoryManager

* Review fixups 2

* Review fixups 3
2026-08-10 18:38:37 +01:00
Stefan Ceriu 42a7b6e8ee Annotate failures for Github Actions so they're easier to spot. 2026-08-10 20:33:43 +03:00
Stefan Ceriu b829c6a06f Prevent invalid snapshots on timeline replies by waiting for the keyboard to appear for longer 2026-08-10 14:49:09 +03:00
Stefan Ceriu 66f68093f0 Remove the createSpan method entirely
As it's easier to live without it than to deal with the API redesign or or misuse
panics.
2026-08-10 13:23:17 +03:00
Stefan Ceriu bb0713b686 Fix tracing span panic when accessed from multiple threads
Brings it in sync with the TimelineItemProvider as it happens.
2026-08-10 13:23:17 +03:00
Stefan Ceriu 0856fdc146 Improve integration test diagnostics in an attempt to catch run crashes 2026-08-10 13:19:41 +03:00
Doug 45890c71a2 Set the finalised OAuth Client URI path to match the new EXI page. (#5992) 2026-08-10 10:30:46 +01:00
ElementRobot 3f6021bcd1 Translations update (#5999)
Co-authored-by: Element CI <ci@element.io>
2026-08-10 09:50:06 +01:00
renovate[bot] 76c34e29bb Update zizmorcore/zizmor-action action to v0.6.2 2026-08-10 07:54:46 +03:00
Doug e3be2981ff Enable automatic call status under the User Status feature flag. (#5994) 2026-08-07 18:26:54 +01:00
Doug 03c434074a Show loading/failure states when setting/clearing user status. (#5993) 2026-08-07 17:24:19 +01:00
Doug aeffd942ec Update the SDK with a fix for read receipts. (#5991) 2026-08-07 13:41:48 +01:00
Danny 1a72a4e17b Allow tapping the avatar in the room member bottom sheet to view it fullscreen (#5989)
* Extract a shared helper for loading a media preview

The four screens that show a fullscreen avatar each had their own copy of the same
load-and-wrap logic. Move it to MediaPreviewItem.load, with the mime type as a
parameter defaulting to JPEG, and leave each screen's loading indicator where it is.

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>

* Only dismiss a QuickLook preview that is still presented

UIKit forwards dismiss() up the presentation chain when the receiver has nothing
presented, so dismissing the avatar view was dismissing the profile details sheet
also.

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>

* Allow tapping the avatar in the room member bottom sheet to view it fullscreen

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>

---------

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>
2026-08-07 13:11:40 +01:00
mredig bbdbbe1081 Create CallView.Coordinator in ViewModel
preserving it across view rebuilds as phone rotates
2026-08-07 12:36:07 +02:00
Mauro Romito 668fbe1827 pr suggestions 2026-08-07 12:35:57 +02:00
Matthew Hodgson 58e1a0212d Indent blank lines and address avatar crop review feedback
Indent the new method's blank lines to satisfy SwiftFormat's
indent-blank-lines rule, which failed the Tests CI jobs.

Also address review findings on the crop itself:
- Request a 256pt thumbnail box at both donation sites so cropping a
  scaled (aspect-fit) thumbnail to its minor axis keeps enough
  resolution for the share sheet's circle.
- Round the draw offsets to whole pixels so odd-sized crops blit
  instead of resampling.
- Log when fetched avatar data can't be decoded rather than silently
  swapping in the placeholder, and document the non-obvious
  format.scale pinning.
2026-08-07 12:35:57 +02:00
Matthew Hodgson 39805d080e Fix broken avatar aspect ratio in the share sheet and notifications
The media repo serves thumbnails with the scale method, so avatars keep
their source aspect ratio. The system stretches whatever INImage we
donate to fill a circle, distorting any non-square avatar in the share
sheet's conversation suggestions (and notifications, which are donated
the same image).

Centre-crop donated avatars to a square before handing them to the
system, in both the timeline's send message intent donation and the
NSE's communication context.
2026-08-07 12:35:57 +02:00
Doug a82726a991 Add timeouts after verifying the device owner when linking a new device. (#5970)
* Support for waitingForAuth continuation sender during Link new device

* Stop automatically regenerating new QR codes when they expire.

This reverts part of commit e235e3ba81.

* Add a timeout after the Face/TouchID verification when linking a desktop computer.

---------

Co-authored-by: Hugh Nimmo-Smith <hughns@element.io>
2026-08-06 14:56:34 +01:00
Mauro Romito a0521d1fdf improve the contribution docs with preview tests setup 2026-08-06 12:26:54 +02:00
mredig 8684b2c050 review fixups 2026-08-06 12:26:54 +02:00
mredig d7283aa552 Update Contributing.md with snapshot testing instructions 2026-08-06 12:26:54 +02:00
Doug f422208399 Update the SDK and handle some API breaks. (#5986) 2026-08-06 10:54:10 +01:00
Doug d0fca495cb Add an HTTP_PROXY environment variable too (also disabled). (#5984) 2026-08-06 10:49:51 +01:00
Stefan Ceriu 0bddf30de9 Improve the room member part of the UI test user session flows 2026-08-06 11:50:21 +03:00
Mauro Romito 748ec818ac remove map tiler url from matrix wellknown 2026-08-05 16:00:43 +02:00
Doug 80530f0006 Add hooks for the RoomDetails, RoomMemberDetails and UserProfile screens. (#5980)
Also adds an @AppHook macro.
2026-08-05 12:41:18 +01:00
renovate[bot] 68f7f2b301 Update dependency pointfreeco/swift-snapshot-testing to v1.19.4 2026-08-05 08:26:21 +03:00
Element CI 5672b5cfcc Prepare next release 2026-08-04 06:17:31 -07:00
Stefan Ceriu f31207583e Tweak the stalebot configuration
So that it:
* checks PRs without needing a needs info label
* capitalize the Stale label
* groups the issues and PRs configurations separately
2026-08-04 14:44:57 +03:00
Stefan Ceriu 56b790da74 Fix various UI test snapshots 2026-08-04 14:17:07 +03:00
Stefan Ceriu b2d74abf2f Disable the store pausing feature as we encountered cases in which it doesn't work properly. 2026-08-04 13:31:52 +03:00
Stefan Ceriu ee8223f0a9 Raise the accessibility tests execution time allowance
testMediaEventsTimelineScreen timed out on CI after 2 minutes. The spindump
shows the runner parked inside XCUIApplication.terminate() with the app
process alive but completely idle, so the audits had already finished and the
teardown alone exhausted the remaining budget.

The test is also the heaviest one in the plan, 120s on CI against 88s for the
next slowest and only 19s locally, since everything runs with the address and
undefined behaviour sanitizers enabled. Doubling the allowance gives all of
them enough headroom.
2026-08-04 12:49:42 +03:00
Stefan Ceriu c4e5bde5bf Fix the missing accessibility description on the Settings button's avatar
The accessibility audit flagged the avatar node in AvatarSettingsButtonLabel
as having no description, since it carried an accessibility identifier but
never a label.

The avatar is purely decorative, both screens that use it wrap it in a button
that is already labelled with the Settings title, so it is now hidden from
accessibility and the identifier has moved up onto the buttons themselves,
where the UI and integration tests query it.
2026-08-04 12:49:42 +03:00
Stefan Ceriu 4ca7c1a8fa Manually publish room list range updates when the content isn't scrollable 2026-08-04 09:39:42 +03:00
renovate[bot] eabbeffd09 Update actions/stale action to v11 2026-08-03 20:54:31 +03:00
Doug 1e78825b0b Rename properties and sort members in advance. 2026-08-03 15:00:38 +01:00
Kai 3ab0aacabf Add call icon next to members in call (#5941)
* feat: add call icon next to members in call

* feat: use matrix rtc room status for in-call

* chore: revert xcodeproj change

* chore: update preview tests

* chore: Update ElementX.xcscheme

* chore: undo room member status proxy

---------

Co-authored-by: Kai <nankeen@users.noreply.github.com>
2026-08-03 15:00:38 +01:00
Mauro Romito 1407a1d5b4 implemented a11y for galleries 2026-08-03 14:33:12 +02:00
Mauro Romito 56d22104ad fix some bugs: 1. caption being displayed when blank 2. gallery should be tapped even when scanning fails 2026-08-03 11:32:28 +02:00
Mauro Romito d7127562d7 added the strings for the pin banner, the room list and the notification content builder 2026-08-03 11:32:28 +02:00
ElementRobot 9a1db7e664 Translations update (#5965)
Co-authored-by: Element CI <ci@element.io>
2026-08-03 09:56:19 +01:00
Doug 58938dbd4f Fix another layout loop. 2026-08-03 09:32:41 +01:00
Doug ad38d9698a Fix a layout loop. 2026-08-03 09:32:41 +01:00
Doug 7c245acaaf Add a custom TabRail on iPad instead of the top tab bar.
Warning: There's currently an observation tracking feedback loop 😕
2026-08-03 09:32:41 +01:00
Mauro Romito bbc995e2c9 Fix for pagination getting stuck when swiping preview items 2026-08-03 10:12:45 +02:00
Doug 94cd9590b5 Allow clearing your own call status. (#5959)
* Allow clearing your own call status.

* Clarify the names of the SettingsScreenUserStatusRow.Mode cases.
2026-07-31 18:15:42 +01:00
Doug f5a1a7d952 Update the Share Extension's Max Count values in xcodegen too. 2026-07-31 18:11:30 +01:00
Mauro Romito 3eb8075913 use fixed size 2026-07-31 15:14:39 +02:00
Mauro Romito e498271420 Revert "size fitting preview tests for the media events timeline"
This reverts commit 666eee86fa0f5da845268bfebcf0173162649de8.
2026-07-31 15:14:39 +02:00
Mauro Romito dbb14e0df4 size fitting preview tests for the media events timeline 2026-07-31 15:14:39 +02:00
Mauro Romito e32c534b89 pr suggestions 2026-07-31 15:14:39 +02:00
Mauro Romito b044764310 unified the filtering of the gallery items 2026-07-31 15:14:39 +02:00
Mauro Romito 68a277143a use the appropriate timeline kind (to be refactored) 2026-07-31 15:14:39 +02:00
renovate[bot] d8cb49e892 Update Project Dependencies (#5940) 2026-07-31 11:10:22 +01:00
Mauro Romito 5d47bb3c56 set gallery limit to 60 when the gallery ff is on 2026-07-31 10:28:43 +02:00
Stefan Ceriu 6d77a0a557 Fix typo in contributing.md 2026-07-31 10:26:57 +03:00
Daniel Anderson 06753c7748 Fix rageshake upload progress bar
The Swift 6 migration consumed the KVO progress publisher through its AsyncSequence, which wants one value at a time. Progress updates that arrive while the loop is busy are dropped, the buffer holds onto the latest one until the loop is ready for it.

Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>
2026-07-31 09:26:55 +03:00
renovate[bot] 894b8edc2c Update zizmorcore/zizmor-action action to v0.6.1 2026-07-31 09:24:59 +03:00
Doug 743c22d980 Update the Enterprise submodule. (#5953)
Add a new ClientProxyError case and make our ClientProtocol extension nonisolated.
2026-07-30 15:08:40 +01:00
Mauro ae795df530 Allow previews in the timeline to include filtered galleries (#5943)
* remove the usage of the timeline kind override in favour of getting it properly from the timeline view model

* allow the previews to include gallery items filtered by the allowed types when started outside the gallery

sss

asa

* fix for actions not being forwarded to the timeline right

* pr suggestions
2026-07-30 13:49:22 +02:00
Doug 33f8ce2759 Fix a couple of missing UserStatus values. (#5944)
* Fix the missing UserStatus in RoomMemberProxy.

* Use the hero's status for a room's AvatarHeaderView.
2026-07-30 10:33:11 +01:00
Mauro 704b3dbb29 Render replies to gallery items (#5942) 2026-07-29 16:28:46 +02:00
Mauro Romito 58fb7c4fcb Wait on durable state in three flaky test suites
These three suites assert against state that is still being loaded or has
already moved on, so they fail whenever a busy runner stretches the window.
All three passed on CI only because `-retry-tests-on-failure` re-ran them.

- MediaUploadPreviewScreenViewModelTests: the processing failure tests treated
  a one second `deferFailure` window as if it also meant the send had finished.
  Preprocessing the valid files outlasts it, so wait for the interaction to be
  re-enabled instead.
- SecurityAndPrivacyScreenViewModelTests: the screen loads its parent spaces and
  its room directory visibility in two independent tasks, and `isSaveDisabled`
  needs both. Every wait now gates on the visibility having landed too.
- RoomNotificationSettingsScreenViewModelTests: the delete tests waited on a
  transient `deletingCustomSetting` flag that the observation coalesces away
  under load. They now wait on the dismiss action and the alert.
2026-07-29 11:53:17 +02:00
Element CI c296eb0e43 Prepare next release 2026-07-28 10:57:41 -07:00
Mauro Romito 55bbc301ef update sdk to 26-7-28 2026-07-28 19:40:53 +02:00
Mauro Romito 30bc4a06f2 fix for a crash when trying to download a media 2026-07-28 16:49:08 +02:00
Mauro Romito 7ea9aeb174 Open a full-screen preview when tapping a gallery item
Tapping a gallery tile/row opens the existing media preview scoped to that gallery's attachments, paging between them. Introduces a typed MediaPreviewItemID (replacing the stringly-typed preview identifier) and a gallery-scoped TimelineMediaPreviewDataSource; the tap routes through TimelineInteractionHandler/TimelineViewModel. Non-previewable (.other) items are skipped without shifting the tapped index.

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>

just use id instead of previewID

removed horrible multiline ternary

pr suggestions

remove redundant code

remove empty caption display + gallery uses the appropriate media timeline actions
2026-07-28 16:49:08 +02:00
Element CI b4f397ed6a Bump the calendar version ready for the next release 2026-07-28 12:30:51 +02:00
Doug 8c56ecbd04 Add an oAuthClientURIPath option to AppSettings. (#5934)
Left unset until our URL is confirmed.
2026-07-28 09:22:22 +01:00
renovate[bot] e62002e7fc Update actions/checkout action to v7.0.1 (#5935)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-27 17:31:52 +00:00
Mauro 4d7d61dae7 Render galleries with non-visual attachments as a list (#5932)
* Render galleries with non-visual attachments as a list

Adds GalleryListView (with GalleryDivider) for galleries containing a file or audio item, extracting the shared FileTypeIconView from FileRoomTimelineView. GalleryRoomTimelineView picks the list vs the grid via usesListLayout. Tapping to open the preview lands in a follow-up.

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>

* pr suggestions

---------

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
2026-07-27 18:46:15 +02:00
Matthew Hodgson a8c79fdd46 Implements push notifications for LLS via MSC4505 (#5872)
* Render live location share start push notifications in the NSE

Live location sharing starts with a cleartext org.matrix.msc3672.beacon_info
STATE event, which the NSE previously rendered as the generic fallback
notification because NotificationContentBuilder only handled message-like
event content.

- Handle .state(.beaconInfo) in NotificationContentBuilder: sender display
  name as the title (same as other messages) with a new localized body,
  "Started sharing their live location".
- Let beaconInfo state events through NotificationHandler's preprocessing
  (all other state events are still discarded).
- Add notification_live_location_started_body, imported from Localazy.
- Unit tests for the new state-event notification paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* rename processMessageLike in processAsMessage

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-07-27 16:31:47 +02:00
ElementRobot 5262350b40 Translations update (#5931)
* Translations update

* update preview tests

---------

Co-authored-by: Element CI <ci@element.io>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-07-27 14:36:14 +02:00
Stefan Ceriu 27928480d5 Bump the RustSDK to v26.07.23
In order to pull in https://github.com/matrix-org/matrix-rust-sdk/pull/6710 and
index more room message types
2026-07-27 10:05:00 +03:00
Mauro 274da99787 Render image and video in galleries as a grid (#5929) 2026-07-24 18:47:39 +02:00
Doug 5d0343c480 Gate user status behind server support as well as the feature flag. (#5920) 2026-07-24 12:23:54 +00:00
Mauro Romito 4a9cd02fbc Add the gallery timeline item rendered as a placeholder
Introduces the GalleryRoomTimelineItem and its typed GalleryItem model (reusing the existing per-message content types), builds it in the RoomTimelineItemFactory and routes it through RoomTimelineItemView. For now it renders as a placeholder; the grid/list media views and the full-screen preview land in follow-up PRs.

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
2026-07-24 11:51:00 +02:00
Doug 3796fdc966 Fix call timeline item snapshots. (#5923)
* Fix call timeline item snapshots.

* Fix a warning and hard code the call timer during tests.
2026-07-23 14:42:44 +01:00
Mauro Romito ae71a92ab7 add content scanner to the NSE Client 2026-07-23 13:42:34 +02:00
Doug d4849a2a79 Remove the requiresExtraAccountSetup badge. (#5921)
It is no longer part of the designs, we only badge the item in Settings.
2026-07-23 09:06:25 +01:00
Doug bf498a5d18 Small tweaks to the Presence feature. (#5918) 2026-07-23 08:40:48 +01:00
Mauro Romito 1770e1d815 Update SDK to 26.07.22 2026-07-22 17:24:19 +02:00
Mauro 6967f99626 Content scanner also checks for thumbnails (#5911)
* scan also thumbnails for medias

* pr suggestion
2026-07-22 16:43:08 +02:00
Jared L cff46137b9 Report user presence on app lifecycle transitions (#5838)
* Report user presence on app lifecycle transitions

Adds a PresenceReporter driven by the app lifecycle handlers and the sharePresence setting: online when foreground-active and sharing, unavailable when backgrounded and sharing, offline when not sharing. Sends are immediate, deduped, and serialized latest-wins. Background app refresh and the NSE seed the process presence to offline so their sync requests carry no presence side effect. Presence rides on the SDK's process-global state, so every sync path is covered without touching sync lifecycle.

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>

* Add presence reporter tests

Pins the presence reporter invariants: the never-online sweep with sharing off, an immediate online send on foreground-active, dedupe of repeated states, latest-wins ordering of serialized sends, and the NSE offline seed.

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>

* wip: address review comments

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>

* refactor: remove obsolete presence heartbeat

Remove the shared activity snapshot, heartbeat refresh, and notification-extension freshness policy while retaining the session-scoped presence service and its serialized lifecycle reporting. Restore the NSE's intermediate one-time offline seed so the cleanup remains buildable ahead of the final sync-presence correction.

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>

* fix: make notification extension presence-neutral

Configure the built NSE client with offline sync presence without sending an immediate presence update. Keep notification setup non-fatal when presence configuration fails and remove the temporary production test seam.

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>

* chore: restore application lifecycle logging

Restore the pre-existing foreground lifecycle diagnostics that were removed during the presence refactor. Keep presence reporting session-scoped and clarify that background refresh configures the client's sync presence without an immediate update.

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>

---------

Signed-off-by: Jared L <48422312+lhjt@users.noreply.github.com>
2026-07-22 15:23:36 +01:00
renovate[bot] e98a19eb3f Update zizmorcore/zizmor-action action to v0.6.0 (#5917)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-22 15:20:16 +01:00
Valere Fedronic 0f72fe3511 Add support for active call item rendering in timeline (#5775)
* feat(timeline): Add support for active call item rendering in timeline

* Update active call as per design (show raw count of members)

* fixup project files

* revert unexpected generated mocks change

* code review

* Record preview snapshots

* revert changes on project.yml

* post rebase fix
2026-07-22 15:13:46 +01:00
Doug 94313173f0 Add a warning to the Link New Device instructions. (#5916) 2026-07-22 14:56:49 +01:00
Doug b46189d05f Add support for showing the user's own status over their avatar on the app's Settings button. (#5912)
* Add a common .inverseMask view modifier and use a blend mode for efficiency.

This also fixes the snapshot rendering 🎉

* Add support for the status emoji on the app's Setting button.

* Align the room header decoration icons with the status emoji.

* Add missing snapshots.
2026-07-22 14:53:32 +01:00
Stefan Ceriu e4f919e58f Update SearchScreen snapshots 2026-07-22 15:40:56 +03:00
Stefan Ceriu c0385ccdfa Change the first search tab name from Rooms to Chats (still untranslated for now) 2026-07-22 15:40:56 +03:00
Stefan Ceriu 6f8c5a0e76 Add more search screen tests 2026-07-22 15:40:56 +03:00
Stefan Ceriu da6eca78f0 Debounce message search only and search the active tab lazily
The search screen previously ran both the room filter and the (slow) message
query on every keystroke regardless of which tab was active, so the fast room
results were held up behind message search.

Now only the active tab is searched, and the other tab is (re)searched when it
becomes active, but only if the query actually changed since it last ran,
tracked per-tab in searchedQueries. Room search reacts to every keystroke while
message search keeps the 250ms debounce, now applied inside the messages branch
of updateFilter rather than upstream of both.
2026-07-22 15:40:56 +03:00
Stefan Ceriu 4eb0863f3a Surface search set query errors with an error indicator 2026-07-22 15:40:56 +03:00
Doug 32a54c5b71 Hook up the emoji picker to the custom status row. (#5902)
* Refactor EmojiPickerScreen to take a continuation instead of a timeline/itemID.

* Hook up the emoji picker to the custom status row.
2026-07-22 13:35:37 +01:00
Doug a6466e80e0 Update the SDK and use RoomHero.status and subscribeToOwnProfile. (#5905)
* Update the SDK and hook up the status from Room Heroes.

Also start requesting profiles via sss when the feature flag is enabled.

* Add support for subscribing to your own profile.
2026-07-22 12:18:02 +01:00
Mauro 4f03acab0c Add signed in flag to the developer options screen hook (#5914)
add signed in flag to the developer options screen hook
2026-07-22 11:40:27 +02:00
Mauro b5b28b9986 Support sending gallery messages behind a feature flag (#5909)
dds sending of multi-media "gallery" messages, gated behind the new galleryEnabled developer option (Developer Options → Room, off by default). Rendering of received galleries ships in a separate PR.

Wires sendGallery through TimelineProxy/TimelineController onto the SDK's Timeline.sendGallery.
MediaUploadPreviewScreen sends selected items as one gallery when the flag is on.
Co-authored with the original author of #5601.

NOTE: This PR allows to send gallery messages, but they won't be rendered without the gallery timeline item PR.
2026-07-21 17:25:30 +02:00
Stefan Ceriu 711cb0a05c Fix trailing_closure warning 2026-07-21 15:07:57 +03:00
Stefan Ceriu ea6475fa1e Fixed unused declaration warnings 2026-07-21 15:07:57 +03:00
Stefan Ceriu 6d74805a65 Disable swiftlint's void_function_in_ternary rule as it yields false positives. 2026-07-21 15:07:57 +03:00
Mauro Romito 5d1e15f308 Revert "Support sending gallery messages behind a feature flag"
This reverts commit 6a2ed331fa.
2026-07-21 12:44:03 +02:00
Mauro Romito 6a2ed331fa Support sending gallery messages behind a feature flag
Adds the ability to send a gallery message (multiple attachments in one message)
via the media upload preview, wired through the timeline controller/proxy to the
SDK's sendGallery. Gated by a `galleryEnabled` developer flag (default off),
matching Element X Android — with the flag off, attachments are sent as
individual messages as before.

Extends the community PR #5601, rebased onto develop. Tracking:
element-hq/element-meta#3201

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
2026-07-21 12:09:07 +02:00
Mauro Romito 762e5ad0ff Revert "Support sending gallery messages behind a feature flag"
This reverts commit 9a61d870d6.
2026-07-21 12:00:03 +02:00
Mauro Romito 9a61d870d6 Support sending gallery messages behind a feature flag
Adds the ability to send a gallery message (multiple attachments in one message)
via the media upload preview, wired through the timeline controller/proxy to the
SDK's sendGallery. Gated by a `galleryEnabled` developer flag (default off),
matching Element X Android — with the flag off, attachments are sent as
individual messages as before.

Extends the community PR #5601, rebased onto develop. Tracking:
element-hq/element-meta#3201

Co-authored-by: svetoslav.mitov <svetoslav.mitov@sportinno.com>
2026-07-21 11:34:44 +02:00
ElementRobot 9354d9d0c2 Re-run xcodegen. (#5907) 2026-07-21 08:49:28 +01:00
Stefan Ceriu d20f62b11b Enable client pausing and resuming for all builds
The mechanism has been running on nightly and debug builds since it was
introduced and Sentry shows the matrix-rust-sdk#6366 background teardown
panic (SwitchGuard::drop) occurring far less on nightly than on production.
Flip the default on for release builds too so store users get the fix.
2026-07-20 16:02:16 +03:00
Mauro 9093631f8b Periphery cleanup (#5893) 2026-07-20 11:16:11 +02:00
Element CI 175aca596e Translations update 2026-07-20 11:09:38 +02:00
Doug e8fffe35d2 Update snapshots. 2026-07-17 17:26:46 +01:00
Jürgen Wischer 99ba9eb235 Fix CI failures: SwiftFormat indentation and stale text field snapshot
Reformat two files to satisfy SwiftFormat's indent rule on blank lines
introduced by the accessibility fixes in this branch, and re-record the
compoundTextFieldStyle preview snapshots to match the new resting-state
border rendering on raised text fields.
2026-07-17 14:14:45 +02:00
Jürgen Wischer 003c608cc5 Revert "Add a visible custom focus ring to the standard button style"
This reverts commit 3f2434c94761877b96fe5bc2e9acc9dfc2b8db2a.
2026-07-17 14:14:45 +02:00
Jürgen Wischer c4f38cf1b2 Use accessibilityValue(_:isEnabled:) instead of an empty-Text fallback
Avoids applying an empty accessibility value when there's only one
pinned item, per review feedback.
2026-07-17 14:14:45 +02:00
Jürgen Wischer 60cd7fd5cc Give the pinned items banner an explicit accessibility label and value
The view-all button drops its visible text to an empty string while
loading, relying only on a ProgressView for feedback, so VoiceOver
users focusing it while it loads hear an unlabelled button. The main
pinned-item button also has no accessibility label/value describing
which pinned item is shown or its position, since children: .contain
leaves that to implicit merging of its sub-views. Set an explicit
label on the view-all button so it always announces its title, and
give the main button an explicit label (the pinned message) and value
(its position, reusing the existing bannerIndicatorDescription/
screenRoomPinnedBannerIndicator strings) instead of relying on
container merging.
2026-07-17 14:14:45 +02:00
Jürgen Wischer 5f6729b737 Mark the room list as frequently-updating for VoiceOver 2026-07-17 14:14:45 +02:00
Jürgen Wischer 5198533242 Add a visible custom focus ring to the standard button style 2026-07-17 14:14:45 +02:00
Jürgen Wischer 814cb99105 Show a resting-state border on raised text fields 2026-07-17 14:14:45 +02:00
Jürgen Wischer ce78d1999e Label the avatar edit button on the own-profile edit screen 2026-07-17 14:14:45 +02:00
Jürgen Wischer 7f5876c079 Expose formatting toolbar buttons as toggle controls to VoiceOver 2026-07-17 14:14:45 +02:00
Jürgen Wischer 34c41cd527 Combine poll option elements and hide the redundant progress bar from VoiceOver 2026-07-17 14:14:45 +02:00
Jürgen Wischer 3d84c611ae Combine the poll winner icon and vote count into one accessibility element 2026-07-17 14:14:45 +02:00
Jürgen Wischer 5e381e17ea Mark the onboarding start screen title as an accessibility header 2026-07-17 14:14:45 +02:00
manuroe 63c99282de Group Element Pro changes into a dedicated release notes section (#5899)
Pro-only PRs (labelled pr-element-pro) leaked into Element X release notes, causing confusion. They now group into a single Element Pro section instead of scattering through Features/Bugfixes. Granularity is dropped on purpose for these changes.
2026-07-17 14:04:25 +02:00
renovate[bot] 2f4dc6e3e4 Update actions/stale action to v10.4.0 (#5898)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-17 12:22:39 +01:00
Doug a0ec5ed9dd Add the UI for your own user status in the Settings screen. (#5888)
* Show the user's own Status emoji on the SettingsScreen.

* Add the UI for setting your own status.

* Add placeholder action for selecting a custom emoji and fix some accessibility issues.
2026-07-17 12:21:51 +01:00
Element CI a4a509f2de Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-16 19:31:11 +02:00
Element CI edaac3eecd Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-16 19:31:11 +02:00
Element CI 2f1cb3eace Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-16 19:31:11 +02:00
Element CI 1158ce61fa Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-16 19:31:11 +02:00
renovate[bot] 5c2c002f3f Update dependency Sentry to v9.21.0 2026-07-16 19:31:11 +02:00
Mauro Romito 3fa61c01b2 update enterprise 2026-07-16 15:58:18 +02:00
renovate[bot] f61c700092 Update dependency pointfreeco/swift-snapshot-testing to v1.19.3 2026-07-16 10:23:33 +02:00
Element CI fd5b3813d4 Prepare next release 2026-07-15 06:36:17 -07:00
Doug a2ba9c4999 Update the SDK. (#5889) 2026-07-15 14:19:50 +01:00
Element CI 9e2696a395 Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-15 14:07:27 +02:00
Element CI 8bc676e2d0 Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-15 14:07:27 +02:00
renovate[bot] 4f29cf6b7a Update Project Dependencies 2026-07-15 14:07:27 +02:00
Element CI 6e3a6b986a Run XcodeGen, update Package.resolved and Settings.bundle 2026-07-15 12:18:46 +02:00
renovate[bot] fd6fb944ad Update dependency Mantis to v3 2026-07-15 12:18:46 +02:00
Mauro 0be5693779 Constrained tests tool implementation + skill (#5873)
* constrained tests tool implementation + skill

* pr suggestions
2026-07-15 12:17:51 +02:00
Matthew Hodgson 1062e2c6dd Add CurrentValuePublisherTests to the UnitTests target (#5884)
Regenerating the project with XcodeGen picks it up.
2026-07-15 07:48:00 +00:00
Mauro 39bd882a27 Fix for flaky tests (#5869)
* possible logging tests fix

* fix for flaky tests

* use timelimit for max test time

* pr suggestions

* remove comment
2026-07-14 18:01:36 +02:00
Doug 85f28d017d Update the SDK and handle API breaks. (#5871)
* Update the SDK and handle API breaks.

* Rename sdkUserThing to rustUserThing for consistency.

* Add user status ClientProxy methods.

* Rename UserStatus.UserSet to UserStatus.Raw.
2026-07-14 16:35:37 +01:00
Doug f776ea7133 Fix a bug where rooms couldn't be opened when returning to the foreground whilst offline. (#5867)
The stores weren't being resumed.
2026-07-14 15:32:19 +01:00
Matthew Hodgson d8a2ac75a8 Fix CurrentValuePublisher.map dropping updates when the mapped publisher is chained inline (#5870)
* Fix CurrentValuePublisher.map dropping updates when the mapped publisher is chained inline

The eager implementation bridged through a second CurrentValueSubject fed by
a sink whose AnyCancellable was stored on the returned struct. Combine
consumes publisher structs when building a subscription, so chaining the
mapped publisher inline (e.g. `publisher.map { … }.removeDuplicates(…)
.sink { … }`, as RoomScreenViewModel and RoomDetailsScreenViewModel do for
knock requests) dropped the only copy holding that cancellable: the feeding
sink was cancelled and subscribers only ever received the initial value.

In practice this meant a room admin viewing a knockable room never saw the
"wants to join" banner (or a live "Requests to join" badge) for a knock
arriving while the room was open — it only appeared when the knock predated
opening the room. Caught by a live cross-client knocking test; the
double-execution of the transform at subscription time (eager transform +
current-value replay) was the giveaway.

Make map lazy instead: keep the transformed AnyPublisher and a value
provider, so subscribers attach straight through to the source subject (which
replays its current value itself) and no keep-alive is needed. Adds a
regression test chaining a mapped publisher inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* swiftformat

* better docs

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
2026-07-14 15:02:56 +02:00
Mauro Romito b730815842 disable creation and enabling of e2ee if remote preference for disabling it is true 2026-07-13 15:51:13 +02:00
Timo 3ba5c08c13 Use dark theme for Element Call (#5860)
* Use dark theme for element Call

* Make sure only the call screen is shown in dark mode.

Otherwise when you go into PiP the entire app has become dark.

---------

Co-authored-by: Doug <douglase@element.io>
2026-07-13 13:28:28 +01:00
Michael c69ebd8898 Fix for NSE and ShareExtension individual builds (#5856) 2026-07-13 12:33:36 +02:00
ElementRobot e8db3cff77 Translations update (#5865)
Co-authored-by: Element CI <ci@element.io>
2026-07-13 11:10:27 +01:00
Mauro e2c573b1eb Use deferFulfillment in flow coordinator tests (#5861)
* use deferFulfillment in coordinator tests

* pr suggestions
2026-07-13 11:52:22 +02:00
Doug 97990b2877 Update the SDK and Compound tokens. (#5857)
* Update Compound Design Tokens.

No changes, just makes some new ones available.

* Update the SDK.
2026-07-13 09:49:15 +01:00
Doug 80ad39e877 Fix UI tests (#5862)
* Fix UI tests compilation error.

* Update UI Snapshots.
2026-07-10 18:12:05 +01:00
Doug 6635113f0b Fix a spacing issue. 2026-07-10 17:59:42 +01:00
Doug aad1b71782 Update the Enterprise submodule. 2026-07-10 17:59:42 +01:00
Doug 73d101c55f Update snapshots. 2026-07-10 17:59:42 +01:00
Doug 23b3fc62cb Show user statuses in the rooms list. 2026-07-10 17:59:42 +01:00
Doug 7b1333c9b8 Show user statuses in the room members list. 2026-07-10 17:59:42 +01:00
Doug e80717f63d Show user statuses in mention suggestions. 2026-07-10 17:59:42 +01:00
Doug 6199ab596f Show user statuses in mention pills. 2026-07-10 17:59:42 +01:00
Doug 01b946926f Show user statuses in the DM room header. 2026-07-10 17:59:42 +01:00
Doug 64908ae642 Show user statuses in the timeline. 2026-07-10 17:59:42 +01:00
Doug bbe6931906 Show user statuses in AvatarHeaderView.
Also tweaks the layout to more closely match Figma.
2026-07-10 17:59:42 +01:00
Doug 2c4775f357 Add the UserStatus and a status field on UserProfile/RoomMember/TimelineItemSender. 2026-07-10 17:59:42 +01:00
3519 changed files with 55253 additions and 19192 deletions
+34
View File
@@ -0,0 +1,34 @@
---
name: constrained-tests
description: Slow-CI simulation — run unit tests on saturated CPU to reproduce CI-style flakiness locally. Suggest when new/reworked suite lean heavy on awaits (deferFulfillment, waitForConfirmation, TestClock, timeouts) or test fail only on CI. Not for every change.
---
# Constrained test runs
CI runners shared + slow. Background tasks starve, awaits stretch, scheduler races flake there, pass locally. `--constrained` saturate CPU with hog tasks — same conditions, local.
## When
- PR add/rework suite heavy on timing-sensitive async: `deferFulfillment`, `waitForConfirmation`, `TestClock`, `transitionValues`, timeouts.
- Test fail on CI, pass locally.
- Not routine changes, not sync suites — slow, no gain.
## How
Full unit test workflow (what CI run):
```bash
swift run tools ci unit-tests --constrained
```
One suite, iterate on new tests (prefer while developing):
```bash
swift run tools ci run-tests --scheme UnitTests --test-name MyNewTests --constrained
```
Hog count automatic (2× cores). Suite survive ~10 constrained iterations → good shape.
## Failures mean
Failure under constraint = real bug in test (sometimes code under test): assertion race async work, transient state observation missed, timeout too tight. Fix synchronisation, no blind timeout bump. Helpers in `UnitTests/Sources/TestUtilities/` (`deferFulfillment`, `deferFailure`, `waitForConfirmation`). Wait on durable state, not transient.
+99
View File
@@ -0,0 +1,99 @@
---
name: periphery-cleanup
description: Dead-code sweep with Periphery — scan the Pro-configured project, delete unused code, guard false positives (keep-alive handles, future-use code), run unit tests, purge scan cache. Use when user want "clean up dead code", "run periphery", "remove unused code".
---
# Periphery cleanup
Periphery = static analysis, find unused code. Scan build ALL schemes — slow (cold ~30-60 min). Maintenance thing, run rare.
## Scan — always on the Pro variant
Scan run on the Pro-configured project so every source get analysed (plain config leave hook implementations + their call sites out, flood report with false "unused"):
```bash
git submodule update --init --recursive # once, if submodules missing
swift run pipeline configure-element-pro
periphery scan --relative-results --quiet > periphery-report.txt
```
Config live in `.periphery.yml` — schemes already cover every target, don't narrow. Two deliberate settings there:
- `retain_unused_protocol_func_params: true` — protocol signatures are contracts (hooks, delegates); params unused in every conformance stay. NEVER annotate protocol function parameters, config already handle them.
- `index_exclude: compound-ios/**` — Compound excluded: its Inspector app + package tests live outside this project's schemes, so their usage invisible → findings there unreliable (e.g. making things internal break the Inspector). Never act on Compound code from this scan.
Report line format: `path:line:col: warning: <kind> '<name>'`. Build fail → fix build first, scan need compiling project.
## Comment hygiene — strip, scan, restore
Every run: existing `periphery:ignore` comments hide drift — code change, comment stay, nobody notice it useless. So:
1. **Strip + remember.** Before the scan, remove every `periphery:ignore` comment (both spellings: `periphery:ignore` and `periphery: ignore`) and stash them: `git diff > /tmp/periphery-comments.patch` right after stripping, or just rely on `git diff` against HEAD to recall what was where.
2. **Scan.** The report now show ground truth: every declaration that genuinely need protection re-flag.
3. **Restore only what re-flag.** For each finding that match the false-positive table, put the comment back (same reason strings). Findings that DON'T re-flag = comments that were stale → stay deleted. Anything newly flagged that isn't a false positive = real dead code → delete it.
Result: comment set always minimal, always true. The clutter never accumulate.
## Triage — kind → action
| Warning | Action |
|---|---|
| Unused function / property / enum / struct / init | Check false-positive table FIRST. True dead → delete whole declaration + helpers/extensions only it used |
| Assign-only property | DANGER ZONE. Prime keep-alive suspect — read table below before touch |
| Unused parameter | Protocol params never appear (config retain them). ObjC selector / system callback → `ignore:parameters <name> - <reason>`. Free signature → remove param + fix call sites |
| Redundant protocol conformance | `TestablePreview` → SKIP, always (see below). Others: verify nothing need conformance (generics, tests, mocks), then remove conformance only, keep type |
| Redundant public accessibility | Drop `public`, make internal — EXCEPT member that satisfy a public protocol in a public type (e.g. `previews`), Swift refuse internal there; skip or internalise whole type |
| Superfluous ignore comment | Declaration referenced now — comment dead, code alive. Remove comment ONLY, never declaration |
Never touch `Generated/` directories, never edit `Localizable.strings`. Delete bottom-up per file — line numbers stay valid.
## False positives — mark, no delete
Stored-never-read variable often alive ON PURPOSE: keep task / SDK object / coordinator alive past declaration scope. Delete = app break silent (observation stop, flow deallocate, rust handle drop). Mark instead: `// periphery:ignore - <reason>` line above declaration (NEVER `periphery:ignore:all` — whole files that shouldn't be reported, like hand-written mocks, belong in `report_exclude` in `.periphery.yml` instead). Comment go ABOVE any doc comment, never between doc and declaration. Reuse existing reasons from codebase:
| Pattern | How recognise | Reason string |
|---|---|---|
| Rust SDK handle | `TaskHandle`, observation token from SDK, stored after `observe`/`subscribe` call | `required for instance retention in the rust codebase` |
| Keep-alive reference | child flow coordinator, service stored so updates keep flowing | `retaining purpose` or `used to avoid deallocation` or `keep alive to keep receiving updates.` |
| Cancellable task handle | `@CancellableTask` property, `Task` stored so reassign/nil cancel previous | `auto cancels when reassigned` |
| SDK enum exhaustiveness | unused `init` switching over SDK enum | `Unused, but added to detect new cases when updating the SDK.` |
| MVVM-C architecture | empty `ViewModelAction` enum from screen template, unused VM protocol — NEVER delete VM protocols, convention keep them | `required for the architecture` (keep the mark even if a superfluous-comment warning appear — it document intent) |
| Property wrapper internals | `@propertyWrapper`, `@dynamicMemberLookup` subscript | `property wrappers generate false positives` / `subscript are seen as false positives` |
| ObjC selector parameter | parameter only touched by runtime (`displayLink`, `sender` etc.) | `ignore:parameters <name> - required for objc selector` |
| Underscored SwiftUI protocol | `_body` from `TextFieldStyle` etc. — called by SwiftUI, members inside resolve transitively | `called by SwiftUI via the TextFieldStyle protocol` |
| SDK delegate requirement | method required by rust SDK delegate protocol (e.g. `didRefreshTokens`) | `required by the SDK's delegate protocol` |
| Synthesized conformance | field only read via synthesized Hashable/Equatable/Codable (cache keys, diffing models) | `used via the synthesized Hashable conformance` |
| Encoded/decoded schema field | Codable field parsed or serialized but not consumed yet | `documents the schema, parsed but not consumed yet` / `part of the encoded payload` |
| Release-only code | compiled out in Debug scan (`#if !DEBUG` paths, nightly checks) | `only used in release builds` |
| `TestablePreview` conformance | flagged "Redundant protocol conformance" on every preview | NO marker, NO removal — Sourcery read conformance, generate snapshot + a11y tests from it. Remove = tests silently vanish. Skip finding, bulk noise |
**Unsure if keep-alive?** Property type Task / AnyCancellable / TaskHandle / coordinator / SDK proxy, assigned inside async or stream setup → smell like keep-alive. ASK USER before delete. Show declaration, where assigned, your read on it.
## Future-use code
Unused but look intentional — API pair completeness, feature half-landed, "team will need soon". No silent delete, no silent keep. Present to user: what, where, opinion keep-or-delete. User say keep → mark `// periphery:ignore - might be useful to have`.
## Mock cascade after protocol deletions
Delete protocol member → Sourcery regenerate mock WITHOUT its `fooReturnValue` / `fooClosure` helpers → hand-written mock `Configuration` inits and test set-up lines still assigning those helpers break the build. Those lines are inert setup for API nothing call — more dead code, not evidence of use. After protocol deletions: grep non-Generated `Mocks/` + `UnitTests/` for `<name>ReturnValue`, `<name>Closure`, direct assignments to deleted members — delete them too. Compiler walk you to the stragglers; that expected, not a false positive.
Careful with same-name members: one mock file often configure SEVERAL protocols (proxy-level `inviter` vs info-level `inviter`). Strip only lines whose target member actually deleted — the wrong one compile fine (member still exist) but silently un-configure previews; snapshot tests catch it, so run them before calling done.
## Verify — on the PLAIN project
Restore the plain configuration BEFORE running tests — snapshots are recorded against plain Element X; Pro branding fail ~8 preview tests falsely:
```bash
git restore project.yml ElementX.xcodeproj/project.pbxproj "ElementX/Resources/AppIcon.icon/Assets/AppIcon.png"
xcodegen
swift run tools ci unit-tests
```
Sourcery regenerate mocks on build — deleted protocol members clean themselves. All tests pass before call done. Note: the CI helper pipe through `xcbeautify` — if not installed the exit code lie even when tests pass; check the `.xcresult` summary.
## Purge cache — always
Periphery DerivedData cache eat tens of GBs. Scan run rare → cache worthless between runs. Last step, no skip:
```bash
rm -rf ~/Library/Caches/com.github.peripheryapp
```
@@ -0,0 +1,323 @@
---
name: releasing-healthchat
description: Cut a HealthChat TestFlight release of element-x-ios by tagging healthchat-v*. Use when the user asks to release, publish, tag, deploy, ship, or cut a new HealthChat iOS version, asks about the TestFlight or App Store release process, asks about renewing the Apple Distribution signing certificate, or asks about the required GitHub Actions secrets for the release pipeline.
---
# Releasing HealthChat (element-x-ios)
Releases are driven by git tags of the form `healthchat-v<version>` pushed to
`origin`. The tag triggers `.github/workflows/release-healthchat.yml`, which runs
on the org's self-hosted macOS runner, builds a signed App Store IPA, and uploads
it to **TestFlight only**. No GitHub Release is created.
**App Store submission is always a manual step** in App Store Connect after a
TestFlight build has been validated — see "Promoting to App Store" below.
**HealthChat ships as an in-place update of the legacy element-ios app.** Same
bundle id (`ch.healthchat.element`), same team (`6G37K72W82`), same App Store
record. Existing users' keychain and app-group container survive the update. Read
"The migration contract" before changing anything that looks like an identifier.
## Preflight checklist
Verify each item before tagging. If any fail, fix and commit first.
- [ ] On branch `release/healthchat` with a clean working tree (`git status`).
- [ ] `HEALTHCHAT_CHANGES.md` has a section `## Changes in <new-version> (<date>)`
at the top. The pipeline's awk extractor matches this exact heading format;
no match ships a fallback placeholder as the TestFlight "What to Test" text.
- [ ] The version is **numeric only** — see "Version rules". This is the single
most common way to waste 30 minutes here.
- [ ] `CHANGES.md` is **not** used for HealthChat releases — upstream's
`release-to-github` owns it. Only edit `HEALTHCHAT_CHANGES.md`.
## Version rules
**Numeric only. One to three dot-separated integers.** `26.7.4`. Never `26.7.4-rc1`.
**Store version ≠ bundle version, by decision (2026-10-16).** App Store Connect shows
the 2.x line (2.0.0 shipped the 26.10.1 build; next is 2.1.0), entered by hand when the
store version is created. The tag/bundle version keeps the calendar scheme and must
exceed the previous one. Apple tolerates the mismatch today but documents that they
"should match" — if that ever gets enforced, align the store label to the bundle version.
The marketing version lands verbatim in `CFBundleShortVersionString`, and Apple
rejects anything that is not at most three period-separated integers. A marker
version passes the tag regex and every local gate, then dies at the **upload
step, after ~30 minutes of signing**, with an ITMS error indistinguishable from a
signing failure.
The Fastfile guards this in `release_testflight`, before any build or signing work. Do not relax that guard.
The legacy element-ios skill documents `-rc1` / `-hotfix1` / `-renew-test` tag
suffixes. **Do not carry that convention over.** The workflow derives the version
by stripping only the `healthchat-v` prefix, so a suffix becomes part of the
version and hits the wall above. Put markers in the release notes, and use the
`workflow_dispatch` path for test builds.
`project.yml:56`'s `MARKETING_VERSION` is only a default — **the tag is the source
of truth**, and the pipeline injects the version at build time without touching
any tracked file. Rebases from upstream will drag `project.yml`'s version around
(`automatic-calendar-version.yml` no-ops in this fork); ignore it.
## Cutting the release (normal path)
```bash
# from release/healthchat, fully committed:
git tag healthchat-v<MAJOR>.<MINOR>.<PATCH>
git push origin healthchat-v<MAJOR>.<MINOR>.<PATCH>
```
Example: `git tag healthchat-v26.7.4`
The `healthchat-` prefix keeps these tags clear of upstream's `release/<version>`
and `nightly/*` namespaces.
The workflow triggers automatically. Watch it with `gh run watch` or the Actions
UI. The TestFlight build appears in App Store Connect within a few minutes of the
run finishing (Apple's processing queue).
**Build numbers are not encoded in tags.** `generate_build_number` in
`fastlane/Fastfile` produces a `YYYYMMDDHHMMSS` timestamp, so ordering is
monotonic and a retry never collides with a build number Apple has already seen.
## Manual / retry path (workflow_dispatch)
Use when retrying a failed run, testing the pipeline (e.g. cert-renewal
verification), or rebuilding without moving tags.
1. GitHub → Actions → "Release HealthChat" → **Run workflow**.
2. Fill in:
- **version** (required): e.g. `26.7.99`. Numeric only — same rule as tags.
- **release_notes_override** (optional): replaces the `HEALTHCHAT_CHANGES.md`
lookup. Use this to mark test builds, since the version cannot carry a marker.
3. Run. The IPA is built and uploaded to TestFlight exactly as the tag path would.
## Release notes
`HEALTHCHAT_CHANGES.md` is the single source. The workflow extracts the section
matching the version being released. Format:
```markdown
## Changes in 26.7.4 (2026-08-01)
🙌 Improvements
- Bullet one.
- Bullet two.
```
No matching section emits a workflow warning and falls back to
`HealthChat <version> — automated build from <ref>.` Prefer fixing the changelog.
A **missing file** is a hard error, not a fallback.
## After the release
- [ ] Workflow run is green in GitHub Actions.
- [ ] Build appears under App Store Connect → TestFlight within ~10 minutes.
- [ ] Internal testers are invited (the auto-distribution group may handle this).
- [ ] Artifacts (IPA, dSYM, Gym logs) attached to the run, retained 90 days.
## Never commit the post-lane tree
A local lane run rewrites `ElementX.xcodeproj/project.pbxproj` — `xcodegen`
regenerates it, then `disable_automatic_code_signing` and
`update_project_provisioning` write Manual signing and profile UUIDs into it. That
file is tracked.
```bash
git checkout -- ElementX.xcodeproj
```
Committing it pins every future build to one machine's profile UUIDs.
## Required GitHub Actions secrets
Five. Missing any fails the workflow at the signing step with a clear error.
| Secret | Source |
|---|---|
| `HEALTHCHAT_ASC_KEY_ID` | App Store Connect → Users and Access → Integrations → Team Keys (10-char Key ID) |
| `HEALTHCHAT_ASC_KEY_ISSUER_ID` | Same page, UUID at the top |
| `HEALTHCHAT_ASC_KEY_CONTENT` | `.p8` contents including the `-----BEGIN/END PRIVATE KEY-----` lines |
| `HEALTHCHAT_CERTIFICATES_P12` | Base64 distribution cert (`base64 -i healthchat.p12 \| pbcopy`) |
| `HEALTHCHAT_CERTIFICATES_P12_PASSWORD` | Password used when exporting the `.p12` |
**There is no `HEALTHCHAT_KEYCHAIN_PASSWORD`.** The legacy skill lists one and the
legacy workflow references it, but it exists at neither repo nor org level and
that pipeline runs green anyway — `import-codesign-certs` tolerates its absence.
Do not add it back.
Managing these needs the `snowping` GitHub account; this repo is INTERNAL and
invisible to the other account.
## Self-hosted runner requirements
Runs on `novaloop-mac-studio-001` — an **org-level** runner (`[self-hosted,
macOS, ARM64]`) shared with the legacy fork, not registered per-repo. One machine,
three names: `novaloop-mac-studio-001` in GitHub, hostname `novaloop-mac-studio-2`
(macOS appended `-2` in October 2026), `novaloops-mac-studio` on Tailscale.
- **One Xcode, globally selected.** A single install at `/Applications/Xcode.app`
(**27.1**, build `27A9275`, since 2026-10-16; upgraded in place via the App Store)
— no versioned `Xcode_*.app` siblings. Upstream 26.10.3+ needs the iOS 27 SDK.
**Nothing pins it.** The Fastfile builds with whatever `xcode-select` points at, so
**whatever the runner has is what ships**. Upgrading the runner's Xcode silently
changes the compiler every subsequent release is built with — no tag, no diff, no
review. Treat that upgrade as a release-affecting change.
- **Do not reintroduce a version pin.** Pinning a version the runner lacks sends
`xcodes` into a multi-GB authenticated download that no CI secret can satisfy, and
the run hangs. Change the runner's Xcode instead.
- **Toolchain installed once by hand**, never by CI: `xcodegen swiftlint swiftformat
xcbeautify xcresultparser pkl` (`xcresultparser` is not in homebrew-core — only
`a7ex/homebrew-formulae/xcresultparser` ships it). `git-lfs`, `xcodes`, `brew` and
rbenv `ruby`/`bundle` are already present. There is deliberately **no `brew bundle`
step and no `Brewfile`**: this machine is shared with CI, and a per-run
`brew update` could upgrade `xcodegen` underneath a running release. CI asserts the
tools are present and fails loudly naming the fix; it never provisions.
- **No passwordless sudo.** Nothing in CI may call `sudo` — that is why upstream's
`setup_github_actions_environment` cannot run here.
- **`APPLE_ID`** exported as a machine-level shell variable — the `Appfile` reads
it and it is not carried in any secret.
- Network access to `developer.apple.com`, `api.appstoreconnect.apple.com`,
`github.com`, `rubygems.org`.
**A release's only Xcode provenance is its own log.** `release-healthchat.yml` runs
`xcodebuild -version` (`Record toolchain`) before every build, so each run records the
compiler that produced it. Nothing pins that version — upgrading the runner's Xcode
silently changes the compiler every subsequent release is built with, and that log
line is the only place it shows up. Do not reconstruct a past build's toolchain from
the runner: it answers for today, not for the build you are asking about. Read the
run's log.
## Renewing the Apple Distribution certificate (annual)
Apple Distribution certificates are valid for **exactly 1 year**. There is no
longer-lived option. Last renewed **2026-04-07**, so the next is due around
**April 2027**. Renew proactively.
1. **~4 weeks before expiry**, check on the Mac holding the private key:
```bash
security find-certificate -c "Apple Distribution: E-HealthSolutions GmbH" -p | openssl x509 -noout -enddate
```
2. **Generate a CSR** on whichever Mac will hold the new private key: Keychain
Access → Certificate Assistant → Request a Certificate from a Certificate
Authority… → "Saved to disk".
3. **Apple Developer portal → Certificates** → **+** → Apple Distribution → upload
the CSR → download the `.cer` → double-click to install.
4. **Regenerate all 3 App Store provisioning profiles** — they reference the old
cert and become invalid:
- `HealthChat App Store`
- `HealthChat NSE: App Store`
- `HealthChat Share Extension: App Store`
5. **Verify locally**: archive in Xcode → Distribute App → App Store Connect →
Upload. TestFlight accepting the build proves the cert works.
6. **Export as `.p12`**: Keychain Access → My Certificates → right-click the new
`Apple Distribution: E-HealthSolutions GmbH` → Export → set a password.
7. **Re-encode and update secrets**: `base64 -i healthchat-new.p12 | pbcopy`, then
replace `HEALTHCHAT_CERTIFICATES_P12` and `HEALTHCHAT_CERTIFICATES_P12_PASSWORD`.
8. **Run the workflow** via `workflow_dispatch` with a throwaway numeric version
to verify end-to-end. Discard the resulting build.
9. **Revoke the old cert** once the new one works in both Xcode and CI.
**Keep the expired `.p12` archived for 2 weeks** in case a rollback is needed.
## Promoting a TestFlight build to the App Store
TestFlight and App Store builds are the same binary. To release a validated one:
1. App Store Connect → My Apps → HealthChat → **TestFlight** → find the build.
2. Confirm it passed internal testing.
3. **Distribution** tab → **+** next to "iOS App" → "Prepare for Submission" →
fill in metadata → **Add for Review**.
4. Wait for review (24–48 hours typical).
5. Release manually, or configure automatic release.
**Deliberately not automated.** Automating submission needs `fastlane/metadata/`
plus HealthChat-branded screenshots (neither exists) and a `Deliverfile`
refactored for API key auth.
**Before the first submission**, confirm the install base against the iOS floor:
element-x requires **iOS 18.5**, the legacy required 15.1. Every user below 18.5
stays on 1.4.1 forever — the App Store will not offer them the update, silently.
That floor is upstream's and cannot be lowered.
## Rollback
- **Bad build, notes fine** → fix, commit, tag the next patch version. TestFlight
supersedes the bad build. (Build numbers are timestamps, so no version games.)
- **Bad release notes only** → edit `HEALTHCHAT_CHANGES.md`, then update the
TestFlight "What to Test" text in App Store Connect by hand — the pipeline only
writes it at upload time.
- **Wrong version number** → treat as burned; tag a higher one.
- **Rolling back to legacy 1.4.1 loses push** — element-x overwrites the user's
sygnal pusher in place on first launch (same app id and pushkey).
## The migration contract
**Read this before "cleaning up" anything.** HealthChat replaces the legacy app in
place, and these are what stop existing users being silently logged out.
- **`CLASSIC_APP_*` in `Variants/HealthChat/healthchat.yml` is load-bearing.**
`ClassicAppManager` reads the legacy account and crypto-store keys through them.
Upstream's defaults in `app.yml` point at Element's own `group.im.vector` /
`im.vector.app.*`, which this team cannot provision. **Never restore upstream's
values.** Failure is silent — `loadAccounts()` throws, and
`AuthenticationService` logs it without telling the user or you.
- **The classic keychain group in `ElementX/SupportingFiles/target.yml` must stay
in the `keychain-access-groups` array.** Entitlement arrays live there, never in
the variant — XcodeGen concatenates arrays on include-merge and only overrides
scalars.
- **The `.requiresBackup` fall-through in `AuthenticationStartScreenViewModel` is a
deliberate upstream divergence.** Upstream sends those accounts to a sheet that
tells the user to open Element Classic. That app no longer exists — this update
replaced it — and the sheet's button opens a URL scheme nothing registers. A
rebase that reverts this reintroduces a dead button.
- **Both apps share the `group.ch.healthchat` UserDefaults suite**
(`AppSettings.swift:56`). An unqualified `@UserPreference` name lands in a suite
the legacy also writes to. Collisions are cheap to introduce.
- **`hasSignedInBefore` voids migration testing.** It lives in that shared suite,
survives app deletion, and short-circuits `loadAccounts()`. Any device that has
run an element-x HealthChat build shows a plain login screen *while migration
works perfectly*. Test on a device that has never run an element-x HealthChat
build, or clear the `group.ch.healthchat` suite first;
`"Already signed in to Element X before, skipping loadAccounts"` in the log
means the run is **void, not failed**. A
`"Classic App not configured, skipping loadAccounts"` line is the opposite —
the `CLASSIC_APP_*` Info.plist keys are missing, `ClassicAppManager` never
initialised, and migration is broken for every user. Investigate, do not
dismiss.
## Known branch divergence on rebase
`fastlane/`, `.github/workflows/release-healthchat.yml`, `HEALTHCHAT_CHANGES.md`,
`Gemfile` and this skill are **fork-only** — upstream element-x-ios has
no `fastlane/` directory at all. A rebase conflict in any of them means something
upstream has started to overlap; keep `--ours` and investigate.
What does need watching:
- **`project.yml`'s `MARKETING_VERSION`** — `automatic-calendar-version.yml` and
`post-release.yml` are gated on `github.repository == 'element-hq/element-x-ios'`
and silently no-op here, so rebases drag upstream's version along. Harmless: the
tag is the source of truth.
- **Upstream files this fork edits** (expect conflicts here on every sync):
`AuthenticationStartScreenViewModel` (migration fall-through, see "The migration
contract"), `AppHooks/UserHooks.swift` (two HealthChat hook fields),
`AppSettings.swift` (VoIP push properties for the NSE), `ElementCallService.swift`
(PushKit token storage, expiration parsing), `NSE/Sources/NotificationHandler.swift`
(VoIP push request to Sygnal), `UserSessionFlowCoordinator` (Members tab),
the `SettingsScreen` files, `RoomMemberDetailsScreen` and `UserProfileScreen`
(extended profile section).
- **Sync strategy: patch stack, like healthchat-web.** `release/healthchat` is
upstream's release tag plus the HealthChat commits on top, nothing else. To sync:
rebase the stack onto the new upstream tag (or rebuild it from the resolved final
tree, one commit per topic), verify tree and tests, then force-push
`release/healthchat`. Older release tags keep their commits alive but leave the
branch line, as on the web fork after its rebase onto v1.12.21. `AGENTS.md`'s
no-rewrite rule is about PR branches; this sync is the one sanctioned rewrite.
- **Unit tests need an iOS 27 simulator.** Upstream's video-size expectations are
encoder-dependent; on iOS 26.5 two `MediaUploadingPreprocessorTests` fail for no
real reason.
- **`app.yml`'s `CLASSIC_APP_*`** — upstream owns these and they stay pointed at
Element's ids. The variant overrides them. Do not "fix" `app.yml`.
@@ -0,0 +1,135 @@
---
name: syncing-upstream-healthchat
description: Bring the HealthChat fork (release/healthchat) onto a newer element-hq/element-x-ios 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, 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. 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.
First done 26.09.1 → 26.10.3 (2026-10-16 to 10-19). Numbers below come from that run.
## Before touching anything
- [ ] **Toolchain.** Upstream pins Xcode + iOS in its CI (`runs-on:` in `.github/workflows/`,
`#available(iOS N)` in sources). Dev Mac AND the runner need it. 26.10.3 needed Xcode 27 /
iOS 27 SDK; the runner still had 26.6 and nobody knew — check `xcodebuild -version` on the
Studio (`novaloops-mac-studio` on Tailscale, runner `novaloop-mac-studio-001`). Upgrade via
App Store in place, open once for the licence, `xcodebuild -downloadPlatform iOS`.
- [ ] **Upstream remote.** `git remote add upstream https://github.com/element-hq/element-x-ios.git`
then `git fetch upstream tag release/<old> tag release/<new> --no-tags`. Release tags are
`release/<version>`. After adding the remote, `gh` resolves to upstream — always pass
`-R Novaloop-AG/element-x-ios`.
- [ ] **A 1.4.1 for the migration test.** Legacy users still exist. No 1.4.1 exists anywhere
(TestFlight builds expire, legacy pipeline last built it 2026-04). Build it from Xcode:
worktree of `Novaloop-AG/element-ios` at tag `healthchat-v1.4.1`, submodule
`matrix-ios-sdk`, `xcodegen`, `pod install`, open `Riot.xcworkspace`, scheme `Riot`,
run on the device with automatic signing. Builds on Xcode 27 (warnings only).
- [ ] **Decisions, product owner:** store version label (stays 2.x, bundle version stays
calendar — see releasing skill), new upstream features on/off (26.10.3: search tab on,
search breadcrumbs kept).
## The stack
```bash
git log --oneline release/<old>..release/healthchat --no-merges # the fork's commits
git merge-base --is-ancestor release/<old> release/healthchat # must be true
```
Upstream files the fork edits — conflicts live here every time:
`AuthenticationStartScreenViewModel` (migration fall-through, see releasing skill "The
migration contract"), `AppHooks/UserHooks.swift` (two HealthChat hook fields),
`AppSettings.swift` (VoIP push properties for the NSE), `ElementCallService.swift` (PushKit
token, expiration parsing), `NSE/Sources/NotificationHandler.swift` (VoIP push to Sygnal),
`UserSessionFlowCoordinator` (Members tab), `SettingsScreen*` (account status section, Labs
gate), the five help URLs are `URL?` (`AppSettings`, `SessionVerificationScreen*`,
`IdentityConfirmationScreen*`, `SecureBackupScreen*`, `RoomScreenModels` + `RoomScreenFooterView`,
`SecurityAndPrivacyScreenModels`, `TimelineViewModel` alert — nil hides the "Learn more" link), `UnitTests/Sources/AnalyticsTests.swift` (suite skipped without PostHog secrets),
`Components/Secrets/Secrets.swift` (analytics/sentry/rageshake nil, hand-edited; upstream's setup
marks it `assume-unchanged`, so run `git update-index --no-assume-unchanged` on it before an
edit shows up in `git status` or a commit),
`RoomMemberDetailsScreen*` + `UserProfileScreen*` (extended profile section), the five fork-gated
CI workflows, `.gitignore`, `project.pbxproj` (never resolve by hand — `xcodegen`).
## Two ways to rebuild the stack
**A. Plain rebase** — `git rebase --onto release/<new> release/<old> release/healthchat` on a
scratch branch, resolve per commit. Faithful, but every conflicting commit must be resolved in
its own context; upstream API removals (26.10.3: `AppHooks.configure(with:)`, `HomeTabsHook`,
`CommonSettingsProtocol`, `appSettings` out of flow parameters) cannot be "resolved", they
need new code. Expect rebased intermediates that do not compile; only the tip has to.
**B. Resolve once, assemble the stack** (used for 26.10.3):
1. Scratch branch off `release/healthchat`, `git merge --no-commit release/<new>`, resolve all
conflicts once, add the rework commits until it builds and tests pass. This is the
*final tree*. Verify it fully (next section) — the stack only reproduces it.
2. Scratch branch off `release/<new>`, rebase the original commits with conflicts resolved to
the fork side blindly (`git checkout --theirs`), then append fix commits whose content is
`git checkout <final tree> -- <files>` per topic:
`chore(healthchat): resolve the rebase onto upstream <new>` (leftovers: workflows, package
pins, Enterprise pointer, start screen), one commit per rework topic, `test(healthchat):
refresh snapshots after rebase onto <new>`, docs. Order of fix commits: resolution first.
3. `git diff --stat <final tree> HEAD` must be empty.
Reproducing historic commits: commit with `--no-verify`, else the SwiftFormat hook rewrites
them. `git cherry-pick` has no `-q`. In zsh, `for c in $VAR` does not split on newlines and a
variable named `path` *is* `PATH` — use `while read`, never name a variable `path`.
## Git LFS — the part that bites
- Checkouts/merges done with `GIT_LFS_SKIP_SMUDGE=1` leave pointer files. Run
`git lfs checkout` before any test run: media fixtures (`UnitTests/Resources`) as pointers
fail `MediaUploadingPreprocessorTests`, `MediaProviderTests`, `VoiceMessageRecorderTests`.
- origin is a standalone repo; upstream's LFS objects are not there. GitHub rejects a push
whose *history* references LFS objects it lacks (`GH008 … unknown Git LFS objects`).
`git lfs fetch upstream release/<new>` gets the tip only. Fetch the tree of every upstream
commit that touched LFS paths, one by one:
```bash
git log --format=%H release/<old>..release/<new> -- '*.png' '*.jpg' '*.gif' '*.mp4' '*.m4a' \
| sort -u | while read c; do git lfs fetch upstream "$c"; done
git lfs push origin <branch> # must report no "(missing)"; then git push
```
A single missing object = one failed fetch; refetch that commit. 26.10.3: 35 commits, ~5 GB.
- Snapshot recording (`swift run tools ci preview-tests --record`, iPhone SE 3rd gen,
iOS version from `Tools/Sources/Commands/CI/CI.swift`) re-records upstream references too
(284 of ~2400 differed on our Mac). Keep only `healthChat*` snapshots; revert upstream's.
## Verification of the final tree
- Build ElementX, NSE, ShareExtension (`xcodebuild build -scheme ElementX` covers all three).
- Unit tests on an **iOS 27+ simulator** matching upstream's CI OS; two video-size tests are
encoder-dependent and fail on older simulators for no reason.
- Migration guard: `healthChatProviderAllowsLegacyAccount` in
`AuthenticationStartScreenViewModelTests` (real hook values → `.welcomeBack`) and
`classicAppAccountRequiresBackupFallsThroughToLogin` must pass.
- Runner: push the branch, `gh workflow run unit-tests.yml --ref <branch>` if the
`pull_request` trigger stays silent. A job whose predecessor was cancelled mid-run can crash
the test host at launch ("Early unexpected exit … signal kill") — plain re-run.
- Device matrix (owner): 1.4.1 → new on a never-used device (log "Already signed in to
Element X before" = void run, "Classic App not configured" = broken), previous release → new,
VoIP ringing on a Debug build (Sygnal `.dev` entries need `platform: sandbox`), push +
badges, Members tab next to any new upstream tab.
## Force-push, rewind
```bash
git push origin <old tip>:refs/heads/backup/release-healthchat-before-<new>
git push --force-with-lease=release/healthchat:<old tip> origin <stack>:release/healthchat
git checkout release/healthchat && git reset --hard origin/release/healthchat # every clone
```
Branch is unprotected: the push lands immediately. Old release 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 TestFlight build made on the new history — tag only after
the device tests. Then update `HEALTHCHAT_CHANGES.md`, the releasing skill (Xcode, divergence
list) and this skill with what changed.
## What not to do
- Squash the fork into a side branch and *merge* upstream there: merging back into the
unsquashed branch conflicts on every file (81 on 26.10.3) and duplicates every commit.
- Resolve `project.pbxproj` by hand. Commit the post-lane project file. Pull on a rewritten
branch (merges the old history back in).
+1 -1
View File
@@ -1,5 +1,5 @@
UITests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text UITests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
DevelopmentAssets/Media/** filter=lfs diff=lfs merge=lfs -text Components/DevelopmentAssets/Media/** filter=lfs diff=lfs merge=lfs -text
UnitTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text UnitTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
PreviewTests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text PreviewTests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
compound-ios/Tests/CompoundTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text compound-ios/Tests/CompoundTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
+1 -1
View File
@@ -6,7 +6,7 @@ git lfs post-checkout "$@"
export PATH="$PATH:/opt/homebrew/bin" export PATH="$PATH:/opt/homebrew/bin"
# ignores updates of 'Secrets.swift' to avoid pushing sensitive data by mistake # ignores updates of 'Secrets.swift' to avoid pushing sensitive data by mistake
git update-index --assume-unchanged Secrets/Secrets.swift git update-index --assume-unchanged Components/Secrets/Secrets.swift
LOCAL_SCRIPT_PATH="$HOME/.local/elementx_local_dev/post-checkout" LOCAL_SCRIPT_PATH="$HOME/.local/elementx_local_dev/post-checkout"
if [ -x "$LOCAL_SCRIPT_PATH" ]; then if [ -x "$LOCAL_SCRIPT_PATH" ]; then
+1
View File
@@ -4,6 +4,7 @@
- [ ] I am aware of the [etiquette](https://github.com/element-hq/element-x-ios/blob/develop/CONTRIBUTING.md#etiquette). - [ ] I am aware of the [etiquette](https://github.com/element-hq/element-x-ios/blob/develop/CONTRIBUTING.md#etiquette).
- [ ] Pull request contains a [changelog label](https://github.com/element-hq/element-x-ios/blob/develop/CONTRIBUTING.md#changelog). - [ ] Pull request contains a [changelog label](https://github.com/element-hq/element-x-ios/blob/develop/CONTRIBUTING.md#changelog).
- [ ] This PR has been made with the help of an LLM. - [ ] This PR has been made with the help of an LLM.
- Model and harness used: <!-- e.g. Claude Opus 5.5 with Claude Code -->
**UI changes have been tested with:** **UI changes have been tested with:**
- [ ] iPhone and iPad simulators in portrait and landscape orientations. - [ ] iPhone and iPad simulators in portrait and landscape orientations.
+4
View File
@@ -28,6 +28,10 @@ changelog:
labels: labels:
- pr-wip - pr-wip
- title: 💼 Element Pro
labels:
- pr-element-pro
- title: Others - title: Others
labels: labels:
- pr-misc - pr-misc
+7 -2
View File
@@ -11,7 +11,12 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests name: Tests
runs-on: macos-26 # HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
# than deleted so the file keeps rebasing cleanly. Run locally with
# `swift run tools ci accessibility-tests`.
if: github.repository == 'element-hq/element-x-ios'
runs-on: xcode-27
timeout-minutes: 150 timeout-minutes: 150
concurrency: concurrency:
@@ -31,7 +36,7 @@ jobs:
run: swift run -q tools ci accessibility-tests run: swift run -q tools ci accessibility-tests
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure() if: failure()
with: with:
name: Results name: Results
@@ -13,14 +13,14 @@ permissions: {}
# Patch bumps are handled by the release script. # Patch bumps are handled by the release script.
jobs: jobs:
automatic-calendar-version: automatic-calendar-version:
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 15 timeout-minutes: 15
# Skip in forks # Skip in forks
if: github.repository == 'element-hq/element-x-ios' if: github.repository == 'element-hq/element-x-ios'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
+13 -8
View File
@@ -20,8 +20,13 @@ permissions: {}
jobs: jobs:
tests: tests:
runs-on: macos-26 # HealthChat: skip in this fork. It only triggers on changes under compound-ios/, which this fork
timeout-minutes: 15 # has never edited, so it would only ever fire on an upstream sync and go red at checkout —
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
# than deleted so the file keeps rebasing cleanly.
if: github.repository == 'element-hq/element-x-ios'
runs-on: xcode-27
timeout-minutes: 25
concurrency: concurrency:
# When running on develop, use the sha to allow all runs of this workflow to run concurrently. # When running on develop, use the sha to allow all runs of this workflow to run concurrently.
@@ -34,8 +39,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Configure Xcode 26 - name: Configure Xcode 27
run: sudo xcode-select -s /Applications/Xcode_26.5.app run: sudo xcode-select -s /Applications/Xcode_27.app
- name: Install sourcery - name: Install sourcery
run: brew install sourcery run: brew install sourcery
@@ -45,11 +50,11 @@ jobs:
run: sourcery --config Tools/Sourcery/PreviewTestsConfig.yml run: sourcery --config Tools/Sourcery/PreviewTestsConfig.yml
- name: Create simulator - name: Create simulator
run: xcrun simctl create "Snapshots" "iPhone SE (3rd generation)" "com.apple.CoreSimulator.SimRuntime.iOS-26-5" run: xcrun simctl create "Snapshots" "iPhone SE (3rd generation)" "com.apple.CoreSimulator.SimRuntime.iOS-27-0"
- name: Run tests - name: Run tests
working-directory: compound-ios working-directory: compound-ios
run: set -o pipefail && xcodebuild test -scheme 'Compound' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=Snapshots,OS=26.5' -skipPackagePluginValidation -resultBundlePath Logs/CompoundTests.xcresult | xcbeautify --renderer github-actions run: set -o pipefail && xcodebuild test -scheme 'Compound' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=Snapshots,OS=27.0' -skipPackagePluginValidation -resultBundlePath Logs/CompoundTests.xcresult | xcbeautify --renderer github-actions
- name: Zip artifacts - name: Zip artifacts
if: failure() # We only care about artefacts if the tests fail if: failure() # We only care about artefacts if the tests fail
@@ -57,7 +62,7 @@ jobs:
run: zip -r Logs/CompoundTests.xcresult.zip Logs/CompoundTests.xcresult/ run: zip -r Logs/CompoundTests.xcresult.zip Logs/CompoundTests.xcresult/
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: failure() # We only care about artefacts if the tests fail if: failure() # We only care about artefacts if the tests fail
with: with:
name: test-results name: test-results
@@ -66,7 +71,7 @@ jobs:
if-no-files-found: ignore if-no-files-found: ignore
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
fail_ci_if_error: false fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
-28
View File
@@ -1,28 +0,0 @@
name: Danger CI
on:
pull_request:
types: [labeled, synchronize, opened, reopened]
permissions: {}
jobs:
build:
name: Danger
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group: ${{ format('danger-ci-{0}', github.ref) }}
cancel-in-progress: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Danger
uses: danger/swift@434d7c25f3b02d490a340b23f5e78dd15a5670bc # 3.22.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+15 -6
View File
@@ -11,7 +11,14 @@ permissions: {}
jobs: jobs:
integration_tests: integration_tests:
name: Integration Tests name: Integration Tests
runs-on: macos-26 # HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, and it is the one
# job that still calls out to Element's Codecov — its upload guard is true on schedule events, so
# it POSTs slug/branch/SHA every night with continue-on-error hiding the failure. Fixing it would
# need a real healthchat.ch test account and three new secrets, and would still not work:
# HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI
# these tests drive. Gated rather than deleted so the file keeps rebasing cleanly.
if: github.repository == 'element-hq/element-x-ios'
runs-on: xcode-27
timeout-minutes: 45 timeout-minutes: 45
concurrency: concurrency:
@@ -20,7 +27,7 @@ jobs:
cancel-in-progress: false cancel-in-progress: false
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
@@ -35,19 +42,21 @@ jobs:
INTEGRATION_TESTS_PASSWORD: ${{ secrets.INTEGRATION_TESTS_PASSWORD }} INTEGRATION_TESTS_PASSWORD: ${{ secrets.INTEGRATION_TESTS_PASSWORD }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
name: Results name: Results
path: test_output/IntegrationTests.xcresult.zip path: |
test_output/IntegrationTests.xcresult.zip
test_output/console*
retention-days: 7 retention-days: 7
if-no-files-found: ignore if-no-files-found: ignore
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
# Skip if not successful and in forks # Skip if not successful and in forks
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
report_type: coverage report_type: coverage
files: test_output/integration-cobertura.xml files: test_output/integration-cobertura.xml
@@ -59,7 +68,7 @@ jobs:
- name: Upload test results to Codecov - name: Upload test results to Codecov
# Skip if cancelled and in forks # Skip if cancelled and in forks
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
continue-on-error: true continue-on-error: true
with: with:
report_type: test_results report_type: test_results
+109
View File
@@ -0,0 +1,109 @@
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:
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:
pull-requests: write
steps:
- name: Check the pull request
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
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
});
const paths = files.map(file => file.filename.toLowerCase());
const warnings = [];
const failures = [];
if (pr.additions > 1000) {
warnings.push("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.");
}
if (!body) {
warnings.push("Please provide a description for this PR.");
}
if (paths.some(path => path.includes("/view")) && !body.includes("user-attachments")) {
warnings.push("You seem to have made changes to views. Please consider adding screenshots.");
}
if (paths.some(path => path.includes(".xcassets") && path.endsWith(".png"))) {
warnings.push("You seem to have made changes to some resource images. Please consider using an SVG or PDF.");
}
if (pr.title.endsWith("…") || /^(Fixes|Fix) #\d+/.test(pr.title) || /^\w+\(\w+\):/.test(pr.title)) {
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 `pr-` label to categorise the changelog entry.");
}
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.`);
}
+2 -2
View File
@@ -10,8 +10,8 @@ permissions: {}
jobs: jobs:
record-snapshots: record-snapshots:
name: Record Preview Snapshots name: Record Preview Snapshots
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 45 timeout-minutes: 60
if: > if: >
github.event.label.name == 'record-snapshots' && github.event.label.name == 'record-snapshots' &&
+133
View File
@@ -0,0 +1,133 @@
name: Release HealthChat
on:
push:
tags:
- 'healthchat-v*'
workflow_dispatch:
inputs:
version:
description: 'Marketing version (e.g. 26.7.4). Must be 1-3 dot-separated integers — Apple rejects anything else.'
required: true
type: string
release_notes_override:
description: 'Optional release notes. If empty, falls back to the HEALTHCHAT_CHANGES.md section for the given version.'
required: false
type: string
permissions:
# Minimum needed for actions/checkout to clone an internal repo. The workflow does not push,
# comment, or create releases — everything else authenticates with the ASC key and .p12.
contents: read
concurrency:
group: release-healthchat-${{ github.event.inputs.version || github.ref_name }}
cancel-in-progress: false
jobs:
release:
name: Build & upload to TestFlight
runs-on: [self-hosted, macOS]
timeout-minutes: 90
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# This repo's only submodule is element-hq's private Enterprise repo, which Novaloop
# cannot clone. Checking out submodules fails the job on its very first step.
submodules: false
persist-credentials: false
- name: Record toolchain
# HealthChat: nothing pins Xcode (the runner carries a single globally-selected one), so this
# log line is the only record of which compiler produced a given TestFlight build.
run: xcodebuild -version
- name: Resolve marketing version
id: version
env:
# Read via env, never `${{ }}` inside the script: the input is attacker-controlled text
# that GitHub would splice into the shell before the regex below could reject it.
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION="$INPUT_VERSION"
else
VERSION="${GITHUB_REF_NAME#healthchat-v}"
fi
# Apple caps CFBundleShortVersionString at three integers. The Fastfile guards this too,
# but failing here costs seconds instead of a signing run.
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "::error::Invalid marketing version '$VERSION' — expected 1-3 dot-separated integers, e.g. 26.7.4"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved marketing version: $VERSION"
- name: Extract release notes
id: notes
env:
VERSION: ${{ steps.version.outputs.version }}
OVERRIDE: ${{ github.event.inputs.release_notes_override }}
run: |
set -euo pipefail
if [[ -n "$OVERRIDE" ]]; then
NOTES="$OVERRIDE"
echo "Using release notes from the workflow input"
elif [[ ! -f HEALTHCHAT_CHANGES.md ]]; then
echo "::error::HEALTHCHAT_CHANGES.md is missing — it is the release-notes source"
exit 1
else
NOTES=$(awk -v ver="$VERSION" '
/^## Changes in / {
if (found) { exit }
if ($0 ~ "## Changes in " ver "( |$|\\()") { found = 1; next }
}
found { print }
' HEALTHCHAT_CHANGES.md | sed '/^[[:space:]]*$/{N;/^\n$/D;}')
if [[ -z "$NOTES" ]]; then
NOTES="HealthChat ${VERSION} — automated build from ${GITHUB_REF_NAME}."
echo "::warning::No HEALTHCHAT_CHANGES.md section found for $VERSION — using fallback"
fi
fi
{
echo 'notes<<NOTES_EOF'
echo "$NOTES"
echo 'NOTES_EOF'
} >> "$GITHUB_OUTPUT"
- name: Bundle install
run: |
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: Import signing certificate
uses: apple-actions/import-codesign-certs@b610f78488812c1e56b20e6df63ec42d833f2d14 # v6.0.0
with:
p12-file-base64: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12 }}
p12-password: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12_PASSWORD }}
- name: Build & upload to TestFlight
run: bundle exec fastlane release_testflight
env:
# Passed via env rather than lane options so multi-line notes containing quotes,
# $, or backticks survive without shell escaping.
HEALTHCHAT_MARKETING_VERSION: ${{ steps.version.outputs.version }}
HEALTHCHAT_RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
APPSTORECONNECT_KEY_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ID }}
APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ISSUER_ID }}
APPSTORECONNECT_KEY_CONTENT: ${{ secrets.HEALTHCHAT_ASC_KEY_CONTENT }}
TEAM_ID: '6G37K72W82'
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: healthchat-${{ steps.version.outputs.version }}
path: |
out/ElementX.ipa
out/ElementX.app.dSYM.zip
build/Logs/Gym/*.log
retention-days: 90
if-no-files-found: warn
+3 -3
View File
@@ -11,7 +11,7 @@ permissions: {}
jobs: jobs:
update-project: update-project:
name: Update Xcode project name: Update Xcode project
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 20 timeout-minutes: 20
permissions: permissions:
contents: write contents: write
@@ -25,7 +25,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
token: ${{ secrets.ELEMENT_BOT_TOKEN }} token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -48,7 +48,7 @@ jobs:
run: xcodebuild -scheme ElementX -project ElementX.xcodeproj -destination 'generic/platform=iOS Simulator' build run: xcodebuild -scheme ElementX -project ElementX.xcodeproj -destination 'generic/platform=iOS Simulator' build
- name: Update Acknowledgements - name: Update Acknowledgements
run: swift-package-list ElementX.xcodeproj --requires-license --ignore-package compound-ios --ignore-package compound-design-tokens --ignore-package matrix-rich-text-editor-swift --ignore-package element-call-swift --output-type settings-bundle --output-path ElementX/SupportingFiles run: swift-package-list ElementX.xcodeproj --requires-license --ignore-package compound-ios --ignore-package compound-design-tokens --ignore-package matrix-rich-text-editor-swift --ignore-package element-call-swift --ignore-package element-call-ios --ignore-package matrix-rust-rtc --output-type settings-bundle --output-path ElementX/SupportingFiles
- name: Commit and push changes - name: Commit and push changes
run: | run: |
+13 -9
View File
@@ -13,17 +13,21 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11
with: with:
only-labels: "X-Needs-Info" labels-to-remove-when-unstale: "X-Needs-Info"
# Issues
only-issue-labels: "X-Needs-Info"
days-before-issue-stale: 30 days-before-issue-stale: 30
days-before-issue-close: 7 days-before-issue-close: 7
days-before-pr-stale: 30 stale-issue-label: "Stale"
days-before-pr-close: 7
stale-pr-label: "stale"
stale-pr-message: "This pull request has seen no activity for the past 30 days and will now be marked as stale. Please update it or leave a comment within the next 7 days to keep it open."
close-pr-message: "This pull request is being closed due to inactivity."
stale-issue-label: "stale"
labels-to-remove-when-unstale: "X-Needs-Info"
stale-issue-message: "This issue has been awaiting further information for the past 30 days so will now be marked as stale. Please provide the requested information within the next 7 days to keep it open." stale-issue-message: "This issue has been awaiting further information for the past 30 days so will now be marked as stale. Please provide the requested information within the next 7 days to keep it open."
close-issue-message: "This issue is being closed due to inactivity after further information was requested." close-issue-message: "This issue is being closed due to inactivity after further information was requested."
# Pull requests
days-before-pr-stale: 30
days-before-pr-close: 7
stale-pr-label: "Stale"
stale-pr-message: "This pull request has seen no activity for the past 30 days and will now be marked as stale. Please update it or leave a comment within the next 7 days to keep it open."
close-pr-message: "This pull request is being closed due to inactivity."
+2 -2
View File
@@ -9,14 +9,14 @@ permissions: {}
jobs: jobs:
open-translations-pr: open-translations-pr:
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 15 timeout-minutes: 15
# Skip in forks # Skip in forks
if: github.repository == 'element-hq/element-x-ios' if: github.repository == 'element-hq/element-x-ios'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
+10 -4
View File
@@ -15,7 +15,13 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests name: Tests
runs-on: macos-26 # HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Fixing the
# permission would not help: the rebrand changed the start screen without re-recording any
# UITests snapshot, so it would fail on content instead. Gated rather than deleted so the file
# keeps rebasing cleanly. Run locally with `swift run tools ci ui-tests`.
if: github.repository == 'element-hq/element-x-ios'
runs-on: xcode-27
timeout-minutes: 90 timeout-minutes: 90
strategy: strategy:
fail-fast: false fail-fast: false
@@ -46,7 +52,7 @@ jobs:
GITHUB_EVENT_INPUTS_TEST_NAME: ${{ github.event.inputs.test_name }} GITHUB_EVENT_INPUTS_TEST_NAME: ${{ github.event.inputs.test_name }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
@@ -58,7 +64,7 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
# Skip if not successful and in forks # Skip if not successful and in forks
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
report_type: coverage report_type: coverage
files: test_output/ui-cobertura.xml files: test_output/ui-cobertura.xml
@@ -70,7 +76,7 @@ jobs:
- name: Upload test results to Codecov - name: Upload test results to Codecov
# Skip if cancelled and in forks # Skip if cancelled and in forks
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
continue-on-error: true continue-on-error: true
with: with:
report_type: test_results report_type: test_results
+26 -4
View File
@@ -13,11 +13,18 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests (Enterprise) name: Tests (Enterprise)
runs-on: macos-26 runs-on: xcode-27
timeout-minutes: 45 timeout-minutes: 60
# Skip in forks # Skip in forks
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} # HealthChat: this job can never pass in this fork — it needs
# secrets.ELEMENT_BOT_TOKEN (Element's bot, absent here) and recursively
# checks out the private element-hq/element-ios-enterprise submodule, which
# Novaloop cannot obtain. The guard below only asks whether the PR comes
# FROM a fork, not whether this repo IS one, so it does not skip us. Gate on
# the upstream repo as post-release.yml does. Gated rather than deleted so
# the file keeps rebasing cleanly against upstream.
if: ${{ github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
concurrency: concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs. # Only allow a single run of this workflow on each branch, automatically cancelling older runs.
@@ -34,14 +41,29 @@ jobs:
- name: Setup environment - name: Setup environment
run: source ci_scripts/ci_common.sh && setup_github_actions_environment run: source ci_scripts/ci_common.sh && setup_github_actions_environment
- name: Restore compilation cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Library/Developer/Xcode/CompilationCache.noindex
key: compilation-cache-unit-tests-enterprise-${{ github.sha }}
restore-keys: compilation-cache-unit-tests-enterprise-
- name: Configure Enterprise - name: Configure Enterprise
run: swift run -q pipeline configure-element-pro run: swift run -q pipeline configure-element-pro
- name: Run tests - name: Run tests
run: swift run -q tools ci unit-tests --skip-previews run: swift run -q tools ci unit-tests --skip-previews
- name: Save compilation cache
# Only develop populates the cache, all of the other runs simply read from it.
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Library/Developer/Xcode/CompilationCache.noindex
key: compilation-cache-unit-tests-enterprise-${{ github.sha }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
+86 -11
View File
@@ -13,27 +13,96 @@ permissions: {}
jobs: jobs:
tests: tests:
name: Tests name: Tests
runs-on: macos-26 # HealthChat: GitHub-hosted macos-26 could not finish this job inside
# timeout-minutes: 60 (two attempts: 51m42s, then killed at 60m44s), and macOS
# minutes bill at 10x. novaloop-mac-studio-001 is the org's Mac Studio, already
# used by the release pipeline; its labels are [self-hosted, macOS, ARM64].
runs-on: [self-hosted, macOS]
# Upstream's 60 is kept: with previews skipped (see Run tests) the build plus unit tests measured
# ~16 min here. A run approaching this budget is a signal, not normal — the earlier 60m timeouts
# were preview tests, not the build.
timeout-minutes: 60 timeout-minutes: 60
permissions:
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout (and on git lfs pull). Upstream is public, hence permissions: {} there.
concurrency: concurrency:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs. # Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group: ${{ format('unit-tests-{0}', github.ref) }} group: ${{ format('unit-tests-{0}', github.ref) }}
cancel-in-progress: true cancel-in-progress: true
steps: steps:
# HealthChat: upstream passes persist-credentials: false here, which the
# composite forwards to actions/checkout — stripping the auth header before
# its separate "Git LFS Pull" step runs. Upstream can afford that because
# its repo is public and git lfs pull works anonymously; this fork is
# INTERNAL, so the LFS batch endpoint would 404. Keep the composite's own
# default (true) so the pull is authenticated with GITHUB_TOKEN.
- uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5 - uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
with:
persist-credentials: false
- name: Setup environment # HealthChat: verify only — never provision. Upstream's
run: source ci_scripts/ci_common.sh && setup_github_actions_environment # setup_github_actions_environment cannot run on this runner: it calls
# `sudo xcode-select -s /Applications/Xcode_26.5.0.app`, but the runner has no
# passwordless sudo and no such path (only /Applications/Xcode.app exists), and
# it calls `brew update && brew install ...` — novaloop-mac-studio-001 is the
# shared release machine, so a per-run `brew update` could upgrade xcodegen
# underneath the release pipeline. These tools are installed once by hand; CI
# only asserts they are present and fails loudly, naming the fix, if not.
- name: Verify environment
run: |
set -eu
missing=""
formulae=""
for tool in xcodegen swiftlint swiftformat xcbeautify xcresultparser pkl git-lfs; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing="$missing $tool"
case "$tool" in
# Not in homebrew-core; only this tap ships it.
xcresultparser) formulae="$formulae a7ex/homebrew-formulae/xcresultparser" ;;
*) formulae="$formulae $tool" ;;
esac
fi
done
if [ -n "$missing" ]; then
echo "::error::Missing on self-hosted runner:${missing}. Install by hand on novaloop-mac-studio-001, then re-run: brew install${formulae}"
exit 1
fi
# The Xcode version is no longer pinned anywhere, so this is the only record
# of which Xcode built a given run.
xcodebuild -version
- name: Restore compilation cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Library/Developer/Xcode/CompilationCache.noindex
key: compilation-cache-unit-tests-${{ github.sha }}
restore-keys: compilation-cache-unit-tests-
- name: Run tests - name: Run tests
run: swift run -q tools ci unit-tests # HealthChat: previews are skipped. PreviewTests renders a snapshot for every preview in the
# app and was the entire reason this job could not fit in an hour — it never once finished,
# on either runner. Unit tests alone take ~16 min here. Upstream skips them the same way in
# unit-tests-enterprise.yml. Cost: preview snapshot regressions are not caught in CI, so
# re-record and check them locally when changing UI.
env:
# HealthChat: VoiceMessageCacheTests cannot write its URLFileProtection.complete cache file on
# this runner's simulator (NSCocoaErrorDomain 513) — a runner-environment issue, not a code bug:
# the suite passes locally and upstream, and neither recreating+booting the device nor clearing
# state fixes it. Skip it here only, via -skip-testing. Drop this once the runner can write
# data-protected files again.
CI_SKIP_TESTS: VoiceMessageCacheTests
run: swift run -q tools ci unit-tests --skip-previews
- name: Save compilation cache
# Only develop populates the cache, all of the other runs simply read from it.
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/Library/Developer/Xcode/CompilationCache.noindex
key: compilation-cache-unit-tests-${{ github.sha }}
- name: Archive artifacts - name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
# We only care about artefacts if the tests fail # We only care about artefacts if the tests fail
if: failure() if: failure()
with: with:
@@ -44,8 +113,12 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
# Skip if not successful and in forks # Skip if not successful and in forks
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} # HealthChat: the existing guard only asks whether the PR comes FROM a
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 # fork, not whether this repo IS one, so it is true here and the step
# runs without a CODECOV_TOKEN — which fails the job via
# fail_ci_if_error. Gate on the upstream repo as post-release.yml does.
if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
with: with:
report_type: coverage report_type: coverage
files: test_output/unit-cobertura.xml,test_output/preview-cobertura.xml files: test_output/unit-cobertura.xml,test_output/preview-cobertura.xml
@@ -56,8 +129,10 @@ jobs:
- name: Upload test results to Codecov - name: Upload test results to Codecov
# Skip if cancelled and in forks # Skip if cancelled and in forks
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} # HealthChat: gated on the upstream repo for the same reason as the
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 # coverage step above — no CODECOV_TOKEN exists in this fork.
if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
continue-on-error: true continue-on-error: true
with: with:
report_type: test_results report_type: test_results
+4 -2
View File
@@ -14,11 +14,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
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.
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout. Upstream is public, hence permissions: {} there.
actions: read # HealthChat: upload-sarif additionally needs this on private/internal repos; public upstream does not.
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Run zizmor 🌈 - name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7 uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
+17 -16
View File
@@ -18,22 +18,9 @@ DerivedData/
.swiftpm .swiftpm
.netrc .netrc
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
towncrier.toml.tmp
version_changes.md
build build
/vendor/ /vendor/
*/xcshareddata/xcodecloud
## macOS Files ## macOS Files
.DS_Store .DS_Store
@@ -47,5 +34,19 @@ build
test_output test_output
# Only commit Pkl packages (there may be other caches in here). # Only commit Pkl packages (there may be other caches in here).
Secrets/vendor/* Components/Secrets/vendor/*
!Secrets/vendor/package-2 !Components/Secrets/vendor/package-2
# fastlane release outputs
out/
provisioning_profiles/
# Regenerated by every fastlane invocation, including on the runner. Ignored rather than
# committed, matching element-ios.
fastlane/README.md
# Review working data (per-machine)
.claude/review-context/
.claude/review-log.jsonl
# Local SDK development override
matrix-rust-components-swift
+22 -10
View File
@@ -1,19 +1,31 @@
project: ElementX.xcodeproj project: ElementX.xcodeproj
# Periphery scans all targets built by these schemes. The ElementX scheme also
# builds NSE, ShareExtension and MatrixRustSDKMocks as dependencies. Always run
# the scan with the project configured for the Pro variant so that all of the
# sources are analysed (see the periphery-cleanup skill).
schemes: schemes:
- ElementX - ElementX
- UnitTests - UnitTests
- PreviewTests - PreviewTests
- UITests - UITests
- IntegrationTests - IntegrationTests
targets: - AccessibilityTests
- ElementX # Compound is excluded: its Inspector app and package tests aren't part of any
- NSE # scheme in this project, so usage from them is invisible to the analysis.
- ShareExtension index_exclude:
- UnitTests - "**/*?.build/**/*"
- PreviewTests - "**/SourcePackages/checkouts/**"
- UITests - "compound-ios/**"
- IntegrationTests
report_exclude: report_exclude:
- ElementX/Sources/Mocks/Generated/GeneratedMocks.swift - "**/Generated/**"
- ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift # Hand-written mocks: configuration surface is deliberately broader than any test uses.
- "**/Mocks/**"
- "ElementX/Sources/Services/AppLock/LAContextMock.swift"
# Pure payload schema, fields exist to be encoded.
- "ElementX/Sources/Services/Notification/Manager/APNSPayload.swift"
# Backports: everything gated on #available, cleaned up with each OS drop.
- "ElementX/Sources/Other/SwiftUI/Backports.swift"
retain_swift_ui_previews: true retain_swift_ui_previews: true
# Protocol signatures are contracts (hooks, delegates) — parameters unused in
# every current conformance are still part of the interface's design.
retain_unused_protocol_func_params: true
+1 -1
View File
@@ -1,6 +1,6 @@
--swiftversion 5.6 --swiftversion 5.6
--exclude **/Sources/**/Generated,vendor,**/Package.swift,Secrets/** --exclude **/Sources/**/Generated,vendor,**/Package.swift,Components/Secrets/**,build
--disable wrapMultiLineStatementBraces --disable wrapMultiLineStatementBraces
--disable hoistPatternLet --disable hoistPatternLet
+2 -1
View File
@@ -3,6 +3,7 @@ disabled_rules:
- unused_setter_value - unused_setter_value
- redundant_discardable_let - redundant_discardable_let
- identifier_name - identifier_name
- void_function_in_ternary
opt_in_rules: opt_in_rules:
- force_unwrapping - force_unwrapping
@@ -17,7 +18,7 @@ included:
excluded: excluded:
- "*/Sources/Generated/*" - "*/Sources/Generated/*"
- Tools/Sources # Our command-line tools use a lot of print commands. - Tools/Sources # Our command-line tools use a lot of print commands.
- Secrets - Components/Secrets
- compound-ios/Inspector - compound-ios/Inspector
line_length: line_length:
+38 -3
View File
@@ -14,6 +14,9 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
- **SwiftLint** (.swiftlint.yml) + **SwiftFormat** (.swiftformat) enforce style on build. Warnings show in Xcode. Build before **XcodeRefreshCodeIssuesInFile**. - **SwiftLint** (.swiftlint.yml) + **SwiftFormat** (.swiftformat) enforce style on build. Warnings show in Xcode. Build before **XcodeRefreshCodeIssuesInFile**.
- Follow [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) everywhere, Rust SDK wrappers too (`ID` not `Id`, `URL` not `Url`, `configuration` not `config` or `cfg`). - Follow [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) everywhere, Rust SDK wrappers too (`ID` not `Id`, `URL` not `Url`, `configuration` not `config` or `cfg`).
- File headers live in `IDETemplateMacros.plist`. - File headers live in `IDETemplateMacros.plist`.
- **Import `SwiftUI`, not `UIKit`** — need `UIPasteboard` etc, `SwiftUI` bring it. `SwiftUI` re-export `Foundation` too, so no `import Foundation` beside it.
- **Clean Code** (Robert C. Martin) Newspaper order: headline first, detail down. Stepdown rule: caller above callee, calls point down. Tests: test cases first, helpers below. Swift idiom always win on clash.
- **Member order in type**: properties (stored + computed, public + private) → `init` → functions. `View` types: properties → `init` → views (`body` first) → functions.
### Comments ### Comments
@@ -52,10 +55,12 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
## Pull Request Guidelines ## Pull Request Guidelines
- New branch from `develop` must not track `develop`.
- Sentence-style titles (no conventional commits). - Sentence-style titles (no conventional commits).
- Exactly one `pr-` label (see `.github/release.yml`). - Exactly one `pr-` label (see `.github/release.yml`).
- Title = changelog entry — descriptive, no "Fixes #…". - Title = changelog entry — descriptive, no "Fixes #…".
- Leave description template for developer. Point them to [contributing etiquette](CONTRIBUTING.md#etiquette). - Leave description template for developer. Point them to [contributing etiquette](CONTRIBUTING.md#etiquette).
- Only fill template LLM line: tick box, name model + harness that made PR (e.g. Claude Opus 5.5, Claude Code).
- Screenshots/videos for visual changes. - Screenshots/videos for visual changes.
- 500 additions max — split big changes. - 500 additions max — split big changes.
- Commits need title + description. No tiny commits, no massive commits. - Commits need title + description. No tiny commits, no massive commits.
@@ -63,6 +68,17 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
--- ---
## Releasing
HealthChat ship via tag `healthchat-v*` → TestFlight. Full runbook in
`.agents/skills/releasing-healthchat/SKILL.md`. `release/healthchat` = upstream tag +
HealthChat commits on top (patch stack). Upstream sync rebase stack, force-push branch —
only rewrite allowed; procedure in
`.agents/skills/syncing-upstream-healthchat/SKILL.md`. Read skill before release, cert
renewal, or touching `fastlane/`. No duplicate runbook here.
---
## Project Structure ## Project Structure
### Build System ### Build System
@@ -269,13 +285,16 @@ Services = where product-level opinions live (Rust SDK stays spec-faithful).
- Inject via `init` parameters. - Inject via `init` parameters.
- Screen coordinators get `Parameters` struct with specific dependencies. - Screen coordinators get `Parameters` struct with specific dependencies.
- `CommonFlowParameters` flow-coordinator-only. - `CommonFlowParameters` flow-coordinator-only.
- `ServiceLocator` **deprecated** — never use services directly. Always inject from above. - `UserSession` = per-account dependency container (`clientProxy`, `userSettings`, `mediaProvider`…).
- Need 2+ from it? Pass `userSession` down. Need 1? Pass that one direct.
- Receiver may store only pieces it need (e.g. `clientProxy`, `userSettings`).
- Never pass `userSession` plus value it already owns (e.g. `userSession` + `userSettings`).
--- ---
## Concurrency & Actors ## Concurrency & Actors
- All targets **Swift 6.2**, approachable concurrency. - All targets **Swift 6** language mode (`SWIFT_VERSION: 6`), approachable concurrency. Compiler version come from Xcode toolchain.
- **ElementX** app, **UnitTests** + **PreviewTests**: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` — no redundant `@MainActor`. Other targets (incl. app extensions) nonisolated. - **ElementX** app, **UnitTests** + **PreviewTests**: `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` — no redundant `@MainActor`. Other targets (incl. app extensions) nonisolated.
- Services + data-layer types: `nonisolated` + `Sendable` when background work need. - Services + data-layer types: `nonisolated` + `Sendable` when background work need.
- Never `@unchecked Sendable` / `nonisolated(unsafe)`. Dev add these. - Never `@unchecked Sendable` / `nonisolated(unsafe)`. Dev add these.
@@ -370,6 +389,22 @@ Same pattern for publishers.
--- ---
## Xcode Agent Skills
Apple ship agent skills with Xcode 27. Installed per machine, not in repo. Skill missing? Suggest user export them (Xcode running): `xcrun agent skills export --output-dir <agent skills dir>` (Claude Code: `~/.claude/skills`). User decline? Skip skill, carry on.
Skill clash this file? This file win. Build settings, entitlements, Info.plist keys, targets → `project.yml`/`target.yml` + `xcodegen`, never `.xcodeproj` or Xcode project-write tools (`UpdateTargetBuildSetting`, `AddEntitlement`…). API newer than target's deployment target → `#available`.
| Skill | Best moment |
|-------|-------------|
| `swiftui-specialist` | Writing/reviewing views: data flow, `ForEach`/`List` identity, environment, perf. `L10n` + Compound tokens beat its samples. |
| `device-interaction` | Runtime check of UI/flow change (subagent skill). Xcode must open this checkout. No login: env `UI_TESTS_SCREEN=<UITestsScreenIdentifier case>` launch that case's mock fixture, no real session. `roomLayout*`, `appLockFlow*`, `*Pagination` need UITests runner. |
| `swiftui-whats-new-27` | SDK 27 `@State`/`@ContentBuilder` compile errors, or adopting iOS 27 SwiftUI APIs. Keep `AlertInfo` alerts. |
| `modernize-tests` | Modernizing UnitTests suites (`@Test(arguments:)`, `withKnownIssue`, `#require`). XCUI targets stay XCTest. Keep `deferFulfillment`/`waitForConfirmation` — bare `confirmation()` no wait async. No `.serialized`/`@MainActor` adds. |
| `app-intents-specialist`, `app-intents-whats-new-27` | Adding App Intents/Shortcuts/Siri/controls (full support planned; iOS 26+ APIs → `-whats-new-27`). Not SiriKit `IN*` code. Intent strings need `LocalizedStringResource` — `L10n` return `String`. |
---
## Key Files ## Key Files
| File | Purpose | | File | Purpose |
@@ -378,7 +413,7 @@ Same pattern for publishers.
| `app.yml` | App-level XcodeGen config | | `app.yml` | App-level XcodeGen config |
| `.swiftlint.yml` | SwiftLint rules | | `.swiftlint.yml` | SwiftLint rules |
| `.swiftformat` | SwiftFormat rules | | `.swiftformat` | SwiftFormat rules |
| `Dangerfile.swift` | Danger PR checks | | `.github/workflows/pr-checks.yml` | PR metadata checks (title, labels, size, screenshots) |
| `Package.swift` | SPM manifest (Tools CLI) | | `Package.swift` | SPM manifest (Tools CLI) |
| `localazy.json` | Localazy translation config | | `localazy.json` | Localazy translation config |
| `codecov.yml` | Codecov config | | `codecov.yml` | Codecov config |
@@ -53,6 +53,10 @@ final class AccessibilityTests: XCTestCase {
private func performAccessibilityAuditForPreview(named name: String) { private func performAccessibilityAuditForPreview(named name: String) {
// Alows us to log the name of the preview that is being tested // Alows us to log the name of the preview that is being tested
XCTContext.runActivity(named: name) { _ in XCTContext.runActivity(named: name) { _ in
// Previews that focus a text field bring up the system keyboard, which we can't fix.
// Snapshot the keyboard in one query so one that's dismissing can't fail the test between checking and reading it.
let keyboardFrame = (try? app.keyboards.firstMatch.snapshot().frame) ?? .null
do { do {
// We have removed `textClipped` and `contrast` for now // We have removed `textClipped` and `contrast` for now
try app.performAccessibilityAudit(for: [.dynamicType, .elementDetection, .hitRegion, .sufficientElementDescription, .trait]) { issue in try app.performAccessibilityAudit(for: [.dynamicType, .elementDetection, .hitRegion, .sufficientElementDescription, .trait]) { issue in
@@ -66,6 +70,11 @@ final class AccessibilityTests: XCTestCase {
return true return true
} }
// The system keyboard's own elements aren't ours to fix
if keyboardFrame.contains(element.frame) {
return true
}
// We are fine with elements that only partially support dynamic types // We are fine with elements that only partially support dynamic types
guard issue.compactDescription != Self.partiallyUnsupportedDynamicTypeMessage else { guard issue.compactDescription != Self.partiallyUnsupportedDynamicTypeMessage else {
return true return true
@@ -98,7 +107,6 @@ final class AccessibilityTests: XCTestCase {
private static let partiallyUnsupportedDynamicTypeMessage = "Dynamic Type font sizes are partially unsupported" private static let partiallyUnsupportedDynamicTypeMessage = "Dynamic Type font sizes are partially unsupported"
private static let notHumanReadableMessage = "Label not human-readable" private static let notHumanReadableMessage = "Label not human-readable"
private static let elementHasNoDescription = "Element has no description"
/// Use this array to filter add specific filters to ignore specific issues for certain elements /// Use this array to filter add specific filters to ignore specific issues for certain elements
private static let ignoredA11yIdentifiers: [String: [FilterType]] = [ private static let ignoredA11yIdentifiers: [String: [FilterType]] = [
@@ -108,11 +116,7 @@ final class AccessibilityTests: XCTestCase {
/// Use this array to filter add specific filters to ignore specific tests /// Use this array to filter add specific filters to ignore specific tests
private static let ignoredA11yTest: [String: [FilterType]] = [ private static let ignoredA11yTest: [String: [FilterType]] = [
// It's an image rendering test doesn't need to have descriptions // It's an image rendering test doesn't need to have descriptions
"RoomAvatarImage_Previews-0": [.auditType(.sufficientElementDescription)], "RoomAvatarImage_Previews-0": [.auditType(.sufficientElementDescription)]
// Empty container slots of the principal search toolbar, system chrome with nothing to describe
"SearchScreen_Previews-Empty": [.emptyContainerDescription],
"SearchScreen_Previews-No results": [.emptyContainerDescription],
"SearchScreen_Previews-Loaded": [.emptyContainerDescription]
] ]
} }
@@ -121,8 +125,6 @@ private enum FilterType {
case compactDescription(String) case compactDescription(String)
/// Filter by the type of the issue /// Filter by the type of the issue
case auditType(XCUIAccessibilityAuditType) case auditType(XCUIAccessibilityAuditType)
/// Filter "no description" issues on empty container elements (label + identifier empty), e.g. system toolbar slots
case emptyContainerDescription
} }
private extension Array where Element == FilterType { private extension Array where Element == FilterType {
@@ -134,14 +136,6 @@ private extension Array where Element == FilterType {
return true return true
case .compactDescription(issue.compactDescription): case .compactDescription(issue.compactDescription):
return true return true
case .emptyContainerDescription:
if issue.auditType == .sufficientElementDescription,
let element = issue.element,
element.elementType == .other,
element.label.isEmpty,
element.identifier.isEmpty {
return true
}
default: default:
break break
} }
@@ -7,8 +7,8 @@
extension AccessibilityTests { extension AccessibilityTests {
func testAdvancedSettingsScreen() async throws { func testActiveCallTimelineItemView() async throws {
try await performAccessibilityAudit(named: "AdvancedSettingsScreen_Previews") try await performAccessibilityAudit(named: "ActiveCallTimelineItemView_Previews")
} }
func testAnalyticsPromptScreen() async throws { func testAnalyticsPromptScreen() async throws {
@@ -55,6 +55,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "AvatarHeaderView_Previews") try await performAccessibilityAudit(named: "AvatarHeaderView_Previews")
} }
func testAvatarSettingsButton() async throws {
try await performAccessibilityAudit(named: "AvatarSettingsButton_Previews")
}
func testBadgeLabel() async throws { func testBadgeLabel() async throws {
try await performAccessibilityAudit(named: "BadgeLabel_Previews") try await performAccessibilityAudit(named: "BadgeLabel_Previews")
} }
@@ -183,6 +187,30 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "FullscreenDialog_Previews") try await performAccessibilityAudit(named: "FullscreenDialog_Previews")
} }
func testGalleryRoomTimelineView() async throws {
try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews")
}
func testHealthChatAccountStatusSection() async throws {
try await performAccessibilityAudit(named: "HealthChatAccountStatusSection_Previews")
}
func testHealthChatExtendedProfileSection() async throws {
try await performAccessibilityAudit(named: "HealthChatExtendedProfileSection_Previews")
}
func testHealthChatInviteMemberScreen() async throws {
try await performAccessibilityAudit(named: "HealthChatInviteMemberScreen_Previews")
}
func testHealthChatMemberDirectoryScreen() async throws {
try await performAccessibilityAudit(named: "HealthChatMemberDirectoryScreen_Previews")
}
func testHealthChatProfileEditScreen() async throws {
try await performAccessibilityAudit(named: "HealthChatProfileEditScreen_Previews")
}
func testHighlightedTimelineItemModifier() async throws { func testHighlightedTimelineItemModifier() async throws {
try await performAccessibilityAudit(named: "HighlightedTimelineItemModifier_Previews") try await performAccessibilityAudit(named: "HighlightedTimelineItemModifier_Previews")
} }
@@ -327,6 +355,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "LocationSharingScreen_Previews") try await performAccessibilityAudit(named: "LocationSharingScreen_Previews")
} }
func testLocationSharingSettingsScreen() async throws {
try await performAccessibilityAudit(named: "LocationSharingSettingsScreen_Previews")
}
func testLoginScreen() async throws { func testLoginScreen() async throws {
try await performAccessibilityAudit(named: "LoginScreen_Previews") try await performAccessibilityAudit(named: "LoginScreen_Previews")
} }
@@ -359,6 +391,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "MediaUploadPreviewScreen_Previews") try await performAccessibilityAudit(named: "MediaUploadPreviewScreen_Previews")
} }
func testMediaUploadQualitySettingsScreen() async throws {
try await performAccessibilityAudit(named: "MediaUploadQualitySettingsScreen_Previews")
}
func testMentionSuggestionItemView() async throws { func testMentionSuggestionItemView() async throws {
try await performAccessibilityAudit(named: "MentionSuggestionItemView_Previews") try await performAccessibilityAudit(named: "MentionSuggestionItemView_Previews")
} }
@@ -379,10 +415,22 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "MessageText_Previews") try await performAccessibilityAudit(named: "MessageText_Previews")
} }
func testModerationAndSafetySettingsScreen() async throws {
try await performAccessibilityAudit(named: "ModerationAndSafetySettingsScreen_Previews")
}
func testMultiAccountAnnouncementView() async throws {
try await performAccessibilityAudit(named: "MultiAccountAnnouncementView_Previews")
}
func testNoticeRoomTimelineView() async throws { func testNoticeRoomTimelineView() async throws {
try await performAccessibilityAudit(named: "NoticeRoomTimelineView_Previews") try await performAccessibilityAudit(named: "NoticeRoomTimelineView_Previews")
} }
func testNotificationCountBadge() async throws {
try await performAccessibilityAudit(named: "NotificationCountBadge_Previews")
}
func testNotificationPermissionsScreen() async throws { func testNotificationPermissionsScreen() async throws {
try await performAccessibilityAudit(named: "NotificationPermissionsScreen_Previews") try await performAccessibilityAudit(named: "NotificationPermissionsScreen_Previews")
} }
@@ -479,6 +527,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "ReadReceiptsSummaryView_Previews") try await performAccessibilityAudit(named: "ReadReceiptsSummaryView_Previews")
} }
func testRedactConfirmationView() async throws {
try await performAccessibilityAudit(named: "RedactConfirmationView_Previews")
}
func testRedactedRoomTimelineView() async throws { func testRedactedRoomTimelineView() async throws {
try await performAccessibilityAudit(named: "RedactedRoomTimelineView_Previews") try await performAccessibilityAudit(named: "RedactedRoomTimelineView_Previews")
} }
@@ -647,10 +699,6 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "SeparatorRoomTimelineView_Previews") try await performAccessibilityAudit(named: "SeparatorRoomTimelineView_Previews")
} }
func testServerConfirmationScreen() async throws {
try await performAccessibilityAudit(named: "ServerConfirmationScreen_Previews")
}
func testServerSelection() async throws { func testServerSelection() async throws {
try await performAccessibilityAudit(named: "ServerSelection_Previews") try await performAccessibilityAudit(named: "ServerSelection_Previews")
} }
@@ -663,6 +711,14 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "SessionVerification_Previews") try await performAccessibilityAudit(named: "SessionVerification_Previews")
} }
func testSettingsScreenUserStatusPickerView() async throws {
try await performAccessibilityAudit(named: "SettingsScreenUserStatusPickerView_Previews")
}
func testSettingsScreenUserStatusRow() async throws {
try await performAccessibilityAudit(named: "SettingsScreenUserStatusRow_Previews")
}
func testSettingsScreen() async throws { func testSettingsScreen() async throws {
try await performAccessibilityAudit(named: "SettingsScreen_Previews") try await performAccessibilityAudit(named: "SettingsScreen_Previews")
} }
@@ -739,10 +795,6 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "SwipeRightAction_Previews") try await performAccessibilityAudit(named: "SwipeRightAction_Previews")
} }
func testSwipeToReplyView() async throws {
try await performAccessibilityAudit(named: "SwipeToReplyView_Previews")
}
func testTextRoomTimelineView() async throws { func testTextRoomTimelineView() async throws {
try await performAccessibilityAudit(named: "TextRoomTimelineView_Previews") try await performAccessibilityAudit(named: "TextRoomTimelineView_Previews")
} }
@@ -787,6 +839,10 @@ extension AccessibilityTests {
try await performAccessibilityAudit(named: "TimelineMediaPreviewRedactConfirmationView_Previews") try await performAccessibilityAudit(named: "TimelineMediaPreviewRedactConfirmationView_Previews")
} }
func testTimelineMessageSelectionActionBar() async throws {
try await performAccessibilityAudit(named: "TimelineMessageSelectionActionBar_Previews")
}
func testTimelineReactionView() async throws { func testTimelineReactionView() async throws {
try await performAccessibilityAudit(named: "TimelineReactionView_Previews") try await performAccessibilityAudit(named: "TimelineReactionView_Previews")
} }
@@ -9,9 +9,6 @@
} }
], ],
"defaultOptions" : { "defaultOptions" : {
"addressSanitizer" : {
"enabled" : true
},
"codeCoverage" : { "codeCoverage" : {
"targets" : [ "targets" : [
{ {
@@ -21,7 +18,7 @@
} }
] ]
}, },
"defaultTestExecutionTimeAllowance" : 120, "defaultTestExecutionTimeAllowance" : 240,
"environmentVariableEntries" : [ "environmentVariableEntries" : [
], ],
@@ -36,8 +33,7 @@
"identifier" : "C0C687DE1D270F9895FEE186", "identifier" : "C0C687DE1D270F9895FEE186",
"name" : "AccessibilityTests" "name" : "AccessibilityTests"
}, },
"testTimeoutsEnabled" : true, "testTimeoutsEnabled" : true
"undefinedBehaviorSanitizerEnabled" : true
}, },
"testTargets" : [ "testTargets" : [
{ {
@@ -36,7 +36,7 @@ targets:
base: base:
PRODUCT_NAME: AccessibilityTests PRODUCT_NAME: AccessibilityTests
PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.accessibility.tests PRODUCT_BUNDLE_IDENTIFIER: ${BASE_BUNDLE_IDENTIFIER}.accessibility.tests
SWIFT_VERSION: 6.2 SWIFT_VERSION: 6
SWIFT_APPROACHABLE_CONCURRENCY: YES SWIFT_APPROACHABLE_CONCURRENCY: YES
configs: configs:
# This is required to remove the $(inherited) flags # This is required to remove the $(inherited) flags
+388
View File
@@ -1,3 +1,391 @@
## Changes in 26.09.2 (2026-09-23)
### What's Changed
🙌 Improvements
* Add an `AccountProvider` type and use it within the authentication flow. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6103
* Update the Start Chat button's icon. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6179
🐛 Bugfixes
* Update the read marker colour to match the Figma. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6146
* Fix some isolation errors when compiling with Swift 6.4. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6148
* Cancel message edit if content remains unchanged by @thepearl in https://github.com/element-hq/element-x-ios/pull/6153
* Fix HDR UI bugs on iOS 27.0. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6177
* Fix the split view layout on iOS 27. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6180
* Only make toolbar buttons prominent when tinted. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6188
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6151
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6164
🚧 In development 🚧
* Introduce a local mechanism for managing the app's badge count by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6135
* Various search tab tweaks by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6157
* Add experimental native calls behind a developer flag by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6159
* Bump element-call-ios to 0.1.0-rc.7 by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6168
* Add specific prefix for native call logs by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6169
* Bump element-call native to 0.1.0-rc.8 by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/6181
Others
* Update Project Dependencies by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6136
* Fix some SwiftLint warnings. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6149
* Update zizmorcore/zizmor-action action to v0.6.4 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6155
* Bump the RustSDK to v26.09.17 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6160
* Update codecov/codecov-action action to v7.1.0 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6176
* Update dependency siteline/SwiftUI-Introspect to v27 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6175
* Drop Intel device support from the build scripts and project by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6171
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6182
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.09.1...release/26.09.2
## Changes in 26.09.1 (2026-09-10)
### What's Changed
✨ Features
* Add a developer option and selection mode for timeline messages by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6102
🐛 Bugfixes
* Limit the custom status input to 30 characters. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6139
* Scope dark mode change only to the presenting sheet by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6144
🚧 In development 🚧
* Show message counts on room list notification badges by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6132
Others
* Fix start chat UI test snapshots following copy changes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6137
* Add a couple of points about code style/ordering to the AGENTS.md by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6133
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6147
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.09.0...release/26.09.1
## Changes in 26.09.0 (2026-09-08)
### What's Changed
✨ Features
* Autoplay the next voice message if it is the next timeline item. by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6089
* Add a Mentions room list filter by @ara4n in https://github.com/element-hq/element-x-ios/pull/5885
* Add support for `<details>` and `<summary>` tags by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6077
* Redaction reason by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6098
🙌 Improvements
* Advance the join room screen if we're autojoined by the server by @ara4n in https://github.com/element-hq/element-x-ios/pull/5875
* Updates to Notification Settings by @americanrefugee in https://github.com/element-hq/element-x-ios/pull/6075
* Use server name from user id function by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6082
🐛 Bugfixes
* Fix crash when activating incompatible room filters by @sergeyospanov in https://github.com/element-hq/element-x-ios/pull/6087
* hide the forward action on live location shares by @thepearl in https://github.com/element-hq/element-x-ios/pull/6091
* Fix photo picker selection tint on iOS 26 by @thepearl in https://github.com/element-hq/element-x-ios/pull/6095
* Fix the tab rail flickering on narrow iPad windows by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6107
* Minor quicklook fixes. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6112
* Use disambiguated display name for avatar change events by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6131
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6088
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6111
🧱 Build
* Pin project dependencies to exact versions given we have Renovate. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6078
* Try using Danger with the Node/Docker fix? by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6079
* Replace danger-swift with a actions/github-script version by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6080
* Bump the calendar version ready for the next release by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6092
Others
* Use the `ClientFactory` pattern everywhere replacing raw `ClientBuilders`. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6054
* Add tests for the UserSessionStore. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6065
* Update Project Dependencies by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6068
* Allow copying blocked user id by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6071
* Don't focus the user status text field during tests. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6076
* Limit link previews width in the timeline to 300 points. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6069
* Fix UI test snapshots following copy changes and filter reordering by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6100
* Feature flag tweaks by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6096
* Update dependency siteline/SwiftUI-Introspect to v26.0.2 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6104
* Update zizmorcore/zizmor-action action to v0.6.3 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6109
* Bump the RustSDK to v26.09.07 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6110
* Update the Enterprise submodule. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6114
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6134
### New Contributors
* @sergeyospanov made their first contribution in https://github.com/element-hq/element-x-ios/pull/6087
* @thepearl made their first contribution in https://github.com/element-hq/element-x-ios/pull/6091
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.08.4...release/26.09.0
## Changes in 26.08.4 (2026-08-25)
### What's Changed
✨ Features
* Enable the User Status feature on supported homeservers. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6059
🐛 Bugfixes
* Attributed string builder tweaks by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6055
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6058
🚧 In development 🚧
* More tweaks for User Status. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6051
Others
* Various tweaks and fixes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6045
* Update dependency element-hq/compound-design-tokens to v10.2.4 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6048
* Update the Enterprise submodule by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6053
* Fix missing back pagination attempt by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6056
* Stop the integration tests from backgrounding the web authentication session by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6060
* Preserve the blank lines typed after a list by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6061
* Update the Enterprise submodule. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6063
* Fixed one accessibility test and an UI test snapshot by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6064
* Update sdk to 26.08.25 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/6067
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.08.3...release/26.08.4
## Changes in 26.08.3 (2026-08-20)
### What's Changed
✨ Features
* Load MapLibre lazily instead of at every app launch by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6012
🙌 Improvements
* Offer Retry and Remove actions with a reason when tapping a failed message by @ara4n in https://github.com/element-hq/element-x-ios/pull/5988
* Update the toolbars in the Chats/Spaces screens. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6043
🐛 Bugfixes
* Avoid race conditions between the home screen's crash alert and Sentry's `onLastRunStatusDetermined` by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6017
* Prevent invalid composer mode crashes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6018
* Delay secondary room summary provider setup by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6038
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6036
🧱 Build
* Re-run XcodeGen. by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6040
🚧 In development 🚧
* Update the SDK fixing some user status bugs. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6037
* Fix the emoji picker's title when setting a custom status. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6039
Others
* Update Project Dependencies by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6011
* Move various top level folder to the `Components` one by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6014
* Update dependency PostHog to v3.69.1 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/6013
* Avoid re-applying identical room list filters by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6016
* Detach the Client's `cacheAccountURL` call by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6019
* Update the Enterprise submodule. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6023
* Fix the flaky ElementCallServiceTests by replacing TestClock by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6022
* Implement TimelineItemProvider diff `truncate` mechanism by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6029
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6030
* Attempt another kind of fix for the encryption settings UI test by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6035
* Fail fast on the integration tests when encountering the first failed assert by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6041
* Various unit test improvements by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6042
* Update the Enterprise submodule. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6049
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.08.2...release/26.08.3
## Changes in 26.08.2 (2026-08-11)
### What's Changed
✨ Features
* Allow tapping the avatar in the room member bottom sheet to view it fullscreen by @danderson-cont in https://github.com/element-hq/element-x-ios/pull/5989
🙌 Improvements
* Remove map tiler url from matrix wellknown by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5983
* Combine the Server Selection screen into the Server Confirmation screen and add autocompletion when entering an account provider. by @mredig in https://github.com/element-hq/element-x-ios/pull/5949
🐛 Bugfixes
* Fix broken avatar aspect ratio in the share sheet and notifications by @ara4n in https://github.com/element-hq/element-x-ios/pull/5972
* Fix Pro Max In Call Rotation Bug by @mredig in https://github.com/element-hq/element-x-ios/pull/5990
* Update the SDK with a fix for read receipts. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5991
* Fix tracing span panic when accessed from multiple threads by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5996
* Make sure the Tab Rail's background matches the split view's detail background. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6003
* Fix a simulcast bug in Element Call. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6009
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5999
🧱 Build
* Add an HTTP_PROXY environment variable too (also disabled). by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5984
* Annotate failures for Github Actions so they're easier to spot. by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6004
* Run XcodeGen by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/6006
* Drop the `addressSanitizer` and `undefinedBehaviorSanitizer` from the Accessibility and UI tests by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6008
* Reinforce text typing within XCUITests by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6007
🚧 In development 🚧
* Add timeouts after verifying the device owner when linking a new device. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5970
* Show loading/failure states when setting/clearing user status. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5993
* Enable automatic call status under the User Status feature flag. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5994
Others
* Update dependency pointfreeco/swift-snapshot-testing to v1.19.4 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5981
* Add hooks for the RoomDetails, RoomMemberDetails and UserProfile screens. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5980
* Improve the room member part of the UI test user session flows by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5985
* Update the SDK and handle some API breaks. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5986
* Update Contributing.md with snapshot testing instructions by @mredig in https://github.com/element-hq/element-x-ios/pull/5974
* Update zizmorcore/zizmor-action action to v0.6.2 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5998
* Set the finalised OAuth Client URI path to match the new EXI page. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5992
* Improve integration test diagnostics in an attempt to catch run crashes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6000
* Prevent invalid snapshots on timeline replies by waiting for the keyboard to appear for longer by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/6001
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/6010
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.08.1...release/26.08.2
## Changes in 26.08.1 (2026-08-04)
### What's Changed
✨ Features
* Render replies to gallery items by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5942
* Render gallery items in the media browser/gallery by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5955
🙌 Improvements
* Allow previews in the timeline to include filtered galleries by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5943
* Set gallery sending limit to 60 when the gallery ff is on by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5960
* Add a custom TabRail on iPad instead of the top tab bar. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5964
* Small gallery tweaks by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5957
* Add call icon next to members in call by @nankeen by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5968
🐛 Bugfixes
* Fix rageshake upload progress bar by @danderson-cont in https://github.com/element-hq/element-x-ios/pull/5950
* Fix for pagination getting stuck loading when swiping media previews in the timeline by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5963
* Manually publish room list range updates when the content isn't scrollable by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5956
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5965
🦻 Accessibility
* A11y for Galleries by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5969
🚧 In development 🚧
* Fix a couple of missing `UserStatus` values. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5944
* Allow clearing your own call status. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5959
Others
* Fix for some flaky tests part 2 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5939
* Update the Enterprise submodule. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5953
* Update zizmorcore/zizmor-action action to v0.6.1 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5961
* Update Project Dependencies by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5940
* Update actions/stale action to v11 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5971
* Accessibility tests fixes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5976
* Disable the store pausing feature as we encountered cases in which it doesn't work properly by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5977
* Fix various UI test snapshots by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5975
* Tweak the stalebot configuration by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5978
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.08.0...release/26.08.1
## Changes in 26.08.0 (2026-07-28)
### What's Changed
✨ Features
* Add support for active call item rendering in timeline by @BillCarsonFr in https://github.com/element-hq/element-x-ios/pull/5775
* Add content scanner to the NSE Client by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5926
* Render image and video in galleries as a grid by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5929
* Render galleries with non-visual attachments as a list by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5932
* Open a full-screen preview when tapping a gallery item by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5936
🙌 Improvements
* Report user presence on app lifecycle transitions by @lhjt in https://github.com/element-hq/element-x-ios/pull/5838
* Content scanner also checks for thumbnails by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5911
* Remove the requiresExtraAccountSetup badge. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5921
* Implements push notifications for LLS via MSC4505 by @ara4n in https://github.com/element-hq/element-x-ios/pull/5872
🐛 Bugfixes
* Enable client pausing and resuming for all builds by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5904
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5901
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5931
🧱 Build
* Re-run xcodegen. by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5907
* Bump the calendar version ready for the next release by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5937
📄 Documentation
* Group Element Pro changes into a dedicated release notes section by @manuroe in https://github.com/element-hq/element-x-ios/pull/5899
🚧 In development 🚧
* Add the UI for your own user status in the Settings screen. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5888
* Support sending gallery messages behind a feature flag by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5909
* Update the SDK and hook up the status from Room Heroes. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5905
* Hook up the emoji picker to the custom status row. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5902
* Search tab tweaks by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5910
* Add support for showing the user's own status over their avatar on the app's Settings button. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5912
* Add a warning to the Link New Device instructions. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5916
* Add the gallery timeline item rendered as a placeholder by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5928
* Gate user status behind server support as well as the feature flag. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5920
Others
* Update dependency pointfreeco/swift-snapshot-testing to v1.19.3 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5892
* Update enterprise by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5894
* Update dependency Sentry to v9.21.0 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5891
* Update actions/stale action to v10.4.0 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5898
* Several small, independent accessibility fixes (VoiceOver labels/traits, contrast) by @unclejay80 in https://github.com/element-hq/element-x-ios/pull/5890
* Periphery cleanup by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5893
* Various project warning fixes by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5908
* Add signed in flag to the developer options screen hook by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5914
* Update zizmorcore/zizmor-action action to v0.6.0 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5917
* Update SDK to 26.07.22 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5919
* Small tweaks to the Presence feature. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5918
* Fix call timeline item snapshots. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5923
* Bump the RustSDK to v26.07.23 by @stefanceriu in https://github.com/element-hq/element-x-ios/pull/5925
* Update actions/checkout action to v7.0.1 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5935
* Add an `oAuthClientURIPath` option to AppSettings. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5934
* update sdk to 26-7-28 by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5938
### New Contributors
* @unclejay80 made their first contribution in https://github.com/element-hq/element-x-ios/pull/5890
* @lhjt made their first contribution in https://github.com/element-hq/element-x-ios/pull/5838
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.07.4...release/26.08.0
## Changes in 26.07.4 (2026-07-15)
### What's Changed
✨ Features
* Force Disable E2EE for Element Pro by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5866
🙌 Improvements
* Use dark theme for Element Call by @toger5 in https://github.com/element-hq/element-x-ios/pull/5860
🐛 Bugfixes
* Fix for NSE and ShareExtension individual builds by @mredig in https://github.com/element-hq/element-x-ios/pull/5856
* Fix CurrentValuePublisher.map dropping updates when the mapped publisher is chained inline by @ara4n in https://github.com/element-hq/element-x-ios/pull/5870
* Fix a bug where rooms couldn't be opened when returning to the foreground whilst offline. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5867
🗣 Translations
* Translations update by @RiotRobot in https://github.com/element-hq/element-x-ios/pull/5865
🧱 Build
* Fix UI tests by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5862
* Add CurrentValuePublisherTests to the UnitTests target by @ara4n in https://github.com/element-hq/element-x-ios/pull/5884
🚧 In development 🚧
* Display other users' User Status. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5859
Others
* Update the SDK and Compound tokens. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5857
* Use deferFulfillment in flow coordinator tests by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5861
* Update the SDK and handle API breaks. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5871
* Fix for flaky tests by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5869
* Constrained tests tool implementation + skill by @Velin92 in https://github.com/element-hq/element-x-ios/pull/5873
* Update dependency Mantis to v3 by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5887
* Update Project Dependencies by @renovate[bot] in https://github.com/element-hq/element-x-ios/pull/5886
* Update the SDK. by @pixlwave in https://github.com/element-hq/element-x-ios/pull/5889
**Full Changelog**: https://github.com/element-hq/element-x-ios/compare/release/26.07.3...release/26.07.4
## Changes in 26.07.3 (2026-07-09) ## Changes in 26.07.3 (2026-07-09)
### What's Changed ### What's Changed
+10 -2
View File
@@ -30,7 +30,7 @@ Only once all of the above is met should you open a PR with your proposed change
* Don't submit large PRs, especially if not previously talked about. Anything above 200 lines is large (excluding generated code e.g. tests, translations, mocks) * Don't submit large PRs, especially if not previously talked about. Anything above 200 lines is large (excluding generated code e.g. tests, translations, mocks)
* Please don't open unfinished PRs and expect us to fill in the details * Please don't open unfinished PRs and expect us to fill in the details
* If you would like our opinion/direction on unfinished code please link your branch or idea in the ticket * If you would like our opinion/direction on unfinished code please link your branch or idea in the ticket
* Please limit the number of commits in a single PR. We are perfectly happy with splitting work across multiple sessions as long as they're logically independant and show promise of progress (ideally expressed through a ticket) * Please limit the number of commits in a single PR. We are perfectly happy with splitting work across multiple sessions as long as they're logically independent and show promise of progress (ideally expressed through a ticket)
* Each and every commit should stand on its own, clearly explaining what it does and why * Each and every commit should stand on its own, clearly explaining what it does and why
* Once a PR goes into review please don't rewrite the history unless agreed so with the reviewer. * Once a PR goes into review please don't rewrite the history unless agreed so with the reviewer.
* Tweaks and fixes following review can be directly committed (to be interactively rebased later) or as fixups * Tweaks and fixes following review can be directly committed (to be interactively rebased later) or as fixups
@@ -95,6 +95,14 @@ git lfs install
If you make changes to the UI you may cause existing UI and Preview test snapshots to fail. The UITests run user flows and record snapshots while doing so using the settings defined under [checkEnvironments](https://github.com/element-hq/element-x-ios/blob/c29175d1f924e58b9646a200dbab0301fce3c258/UITests/Sources/Application.swift#L35-L37) while the PreviewTests use the settings defined in [PreviewTests.swift](https://github.com/element-hq/element-x-ios/blob/c29175d1f924e58b9646a200dbab0301fce3c258/PreviewTests/Sources/PreviewTests.swift#L18-L20). The snapshots are stored under `Sources/__Snapshots__` in their respective target's folder. If you make changes to the UI you may cause existing UI and Preview test snapshots to fail. The UITests run user flows and record snapshots while doing so using the settings defined under [checkEnvironments](https://github.com/element-hq/element-x-ios/blob/c29175d1f924e58b9646a200dbab0301fce3c258/UITests/Sources/Application.swift#L35-L37) while the PreviewTests use the settings defined in [PreviewTests.swift](https://github.com/element-hq/element-x-ios/blob/c29175d1f924e58b9646a200dbab0301fce3c258/PreviewTests/Sources/PreviewTests.swift#L18-L20). The snapshots are stored under `Sources/__Snapshots__` in their respective target's folder.
To run the preview tests:
1. Use an iPhone SE (3rd generation) simulator running iOS 27.0.
2. On that simulator, go to Settings -> General -> Language & Region and select United States as the region, with English (US) as the only preferred language. This is a one time setup, as long as you keep using the same simulator.
3. Select the `PreviewTests` scheme and run the tests.
Newly created screens will automatically have their screenshots added based off their `TestablePreview` Swift previews. If you are modifying an existing screen, you will first need to delete the old snapshots for that screen before running the tests. Alternatively, if you enable `RECORD_FAILURES` in the preview test plans's environment variables (`PreviewTests/SupportingFiles/Previewtests.xctestplan`), it will overwrite snapshot failures with the updated screenshot.
### Githooks ### Githooks
The project uses its own shared githooks stored in the .githooks folder, you will need to configure git to use such folder, this is already done if you have run the setup tool with `swift run tools setup-project` otherwise you would need to run: The project uses its own shared githooks stored in the .githooks folder, you will need to configure git to use such folder, this is already done if you have run the setup tool with `swift run tools setup-project` otherwise you would need to run:
@@ -142,7 +150,7 @@ Our [changelog](CHANGES.md) is automatically generated by GitHub, based on the P
For Swift coding style we use [SwiftLint](https://github.com/realm/SwiftLint) to check some conventions at compile time (rules are located in the `.swiftlint.yml` file). For Swift coding style we use [SwiftLint](https://github.com/realm/SwiftLint) to check some conventions at compile time (rules are located in the `.swiftlint.yml` file).
Otherwise please have a look to [Apple Swift conventions](https://swift.org/documentation/api-design-guidelines.html#conventions). We are also using some of the conventions of [raywenderlich.com Swift style guide](https://github.com/raywenderlich/swift-style-guide). Otherwise please have a look to [Apple Swift conventions](https://swift.org/documentation/api-design-guidelines.html#conventions). We are also using some of the conventions of [raywenderlich.com Swift style guide](https://github.com/raywenderlich/swift-style-guide).
We enforce the coding style by running checks on the CI for every PR through [Danger](Dangerfile.swift), [SwiftLint](.swiftlint.yml), [SwiftFormat](.swiftformat) and [SonarCloud](https://sonarcloud.io/project/overview?id=element-x-ios) We enforce the coding style by running checks on the CI for every PR through [PR Checks](.github/workflows/pr-checks.yml), [SwiftLint](.swiftlint.yml), [SwiftFormat](.swiftformat) and [SonarCloud](https://sonarcloud.io/project/overview?id=element-x-ios)
We also gather coverage reports on every PR through [Codecov](https://app.codecov.io/gh/element-hq/element-x-ios) and will eventually start enforcing minimums. We also gather coverage reports on every PR through [Codecov](https://app.codecov.io/gh/element-hq/element-x-ios) and will eventually start enforcing minimums.
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax", "location" : "https://github.com/swiftlang/swift-syntax",
"state" : { "state" : {
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
"version" : "603.0.2" "version" : "604.0.0"
} }
} }
], ],
@@ -5,12 +5,15 @@ import PackageDescription
let package = Package( let package = Package(
name: "BuildExtensions", name: "BuildExtensions",
platforms: [.macOS(.v15)], platforms: [
.macOS(.v15),
.iOS(.v18) // Only necessary for Previews
],
products: [ products: [
.library(name: "Macros", targets: ["Macros"]) .library(name: "Macros", targets: ["Macros"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax", from: "603.0.2") .package(url: "https://github.com/swiftlang/swift-syntax", from: "604.0.0")
], ],
targets: [ targets: [
// The public macro declarations imported by the app. // The public macro declarations imported by the app.
@@ -0,0 +1,16 @@
//
// Copyright 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.
//
/// Exposes a `Mutex`-backed hook as a read-only computed property, alongside a `register<Name>`
/// peer that swaps in a replacement hook.
///
/// The `default` argument provides the hook used until (and unless) a replacement is registered.
/// The enclosing type must have `Synchronization` in scope for the generated `Mutex` storage.
@attached(accessor)
@attached(peer, names: arbitrary)
public macro AppHook<Value>(default: Value) =
#externalMacro(module: "MacrosImplementation", type: "AppHookMacro")
@@ -16,7 +16,7 @@
/// persisted under a legacy key). /// persisted under a legacy key).
/// ///
/// A `reset<Name>()` method is also generated, which clears the stored value so it reverts to the /// A `reset<Name>()` method is also generated, which clears the stored value so it reverts to the
/// default. /// default. Non-volatile preferences also get a `<name>Key` property exposing the storage key.
@attached(accessor) @attached(accessor)
@attached(peer, names: arbitrary) @attached(peer, names: arbitrary)
public macro UserPreference<Value>(key: String? = nil, defaultValue: Value, volatile: Bool = false) = public macro UserPreference<Value>(key: String? = nil, defaultValue: Value, volatile: Bool = false) =
@@ -27,3 +27,13 @@ public macro UserPreference<Value>(key: String? = nil, defaultValue: Value, vola
@attached(peer, names: arbitrary) @attached(peer, names: arbitrary)
public macro UserPreference(key: String? = nil, volatile: Bool = false) = public macro UserPreference(key: String? = nil, volatile: Bool = false) =
#externalMacro(module: "MacrosImplementation", type: "UserPreferenceMacro") #externalMacro(module: "MacrosImplementation", type: "UserPreferenceMacro")
/// The storage key of a `@UserPreference`, distinct from `String` so that only a generated
/// `<name>Key` property can be passed where a preference key is expected.
public struct UserPreferenceKey: Sendable {
public let rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
}
@@ -0,0 +1,98 @@
//
// Copyright 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.
//
import SwiftSyntax
import SwiftSyntaxMacros
/// Implements `@AppHook`. The generated property reads through a `Mutex`-backed store, and a
/// generated `register<Name>` method swaps in a replacement hook.
enum AppHookMacro {
enum DiagnosticError: Error, CustomStringConvertible {
case notAStoredProperty
case missingTypeAnnotation
case missingDefault
var description: String {
switch self {
case .notAStoredProperty:
"@AppHook can only be applied to a stored property."
case .missingTypeAnnotation:
"@AppHook requires an explicit type annotation."
case .missingDefault:
"@AppHook requires a default value."
}
}
}
private struct AppHook {
let name: String
let type: TypeSyntax
let defaultValue: ExprSyntax
var storageName: TokenSyntax {
"_\(raw: name)"
}
var registerMethodName: TokenSyntax {
"register\(raw: name.prefix(1).uppercased() + name.dropFirst())"
}
}
private static func appHook(for declaration: some DeclSyntaxProtocol, node: AttributeSyntax) throws -> AppHook {
guard let variable = declaration.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw DiagnosticError.notAStoredProperty
}
guard let type = binding.typeAnnotation?.type.trimmed else {
throw DiagnosticError.missingTypeAnnotation
}
var defaultValue: ExprSyntax?
if let list = node.arguments?.as(LabeledExprListSyntax.self) {
for argument in list where argument.label?.text == "default" {
defaultValue = argument.expression
}
}
guard let defaultValue else {
throw DiagnosticError.missingDefault
}
return AppHook(name: identifier.text, type: type, defaultValue: defaultValue)
}
}
extension AppHookMacro: AccessorMacro {
static func expansion(of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
let appHook = try appHook(for: declaration, node: node)
return [
"""
get {
\(appHook.storageName).withLock { $0 }
}
"""
]
}
}
extension AppHookMacro: PeerMacro {
static func expansion(of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
let appHook = try appHook(for: declaration, node: node)
return [
"private let \(appHook.storageName) = Mutex<\(appHook.type)>(\(appHook.defaultValue))",
"""
func \(appHook.registerMethodName)(_ hook: \(appHook.type)) {
\(appHook.storageName).withLock { $0 = hook }
}
"""
]
}
}
@@ -10,5 +10,5 @@ import SwiftSyntaxMacros
@main @main
struct BuildExtensionsPlugin: CompilerPlugin { struct BuildExtensionsPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [UserPreferenceMacro.self] let providingMacros: [Macro.Type] = [UserPreferenceMacro.self, AppHookMacro.self]
} }
@@ -14,7 +14,7 @@ import SwiftSyntaxMacros
/// ///
/// A `volatile` preference instead reads and writes through a generated in-memory store, so its /// A `volatile` preference instead reads and writes through a generated in-memory store, so its
/// value is never persisted and resets on each launch. /// value is never persisted and resets on each launch.
public enum UserPreferenceMacro { enum UserPreferenceMacro {
enum DiagnosticError: Error, CustomStringConvertible { enum DiagnosticError: Error, CustomStringConvertible {
case notAStoredProperty case notAStoredProperty
case missingTypeAnnotation case missingTypeAnnotation
@@ -45,6 +45,10 @@ public enum UserPreferenceMacro {
"_\(raw: name)VolatileValue" "_\(raw: name)VolatileValue"
} }
var keyPropertyName: TokenSyntax {
"\(raw: name)Key"
}
var resetMethodName: TokenSyntax { var resetMethodName: TokenSyntax {
"reset\(raw: name.prefix(1).uppercased() + name.dropFirst())" "reset\(raw: name.prefix(1).uppercased() + name.dropFirst())"
} }
@@ -85,9 +89,9 @@ public enum UserPreferenceMacro {
} }
extension UserPreferenceMacro: AccessorMacro { extension UserPreferenceMacro: AccessorMacro {
public static func expansion(of node: AttributeSyntax, static func expansion(of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol, providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] { in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
let preference = try preference(for: declaration, node: node) let preference = try preference(for: declaration, node: node)
if preference.isVolatile { if preference.isVolatile {
@@ -137,9 +141,9 @@ extension UserPreferenceMacro: AccessorMacro {
} }
extension UserPreferenceMacro: PeerMacro { extension UserPreferenceMacro: PeerMacro {
public static func expansion(of node: AttributeSyntax, static func expansion(of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol, providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [DeclSyntax] { in context: some MacroExpansionContext) throws -> [DeclSyntax] {
let preference = try preference(for: declaration, node: node) let preference = try preference(for: declaration, node: node)
let type = preference.type let type = preference.type
let name = preference.name let name = preference.name
@@ -157,6 +161,14 @@ extension UserPreferenceMacro: PeerMacro {
} }
""") """)
if !preference.isVolatile {
peers.append("""
var \(preference.keyPropertyName): UserPreferenceKey {
.init(\(preference.key))
}
""")
}
peers.append(resetMethod(for: preference)) peers.append(resetMethod(for: preference))
return peers return peers
@@ -5,7 +5,7 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import MacrosImplementation @testable import MacrosImplementation
import SwiftSyntaxMacrosTestSupport import SwiftSyntaxMacrosTestSupport
import XCTest import XCTest
@@ -35,6 +35,10 @@ final class UserPreferenceMacroTests: XCTestCase {
_hasSeenNewSoundBannerSubject.prepend(hasSeenNewSoundBanner).eraseToAnyPublisher() _hasSeenNewSoundBannerSubject.prepend(hasSeenNewSoundBanner).eraseToAnyPublisher()
} }
var hasSeenNewSoundBannerKey: UserPreferenceKey {
.init("hasSeenNewSoundBanner")
}
func resetHasSeenNewSoundBanner() { func resetHasSeenNewSoundBanner() {
store.removeObject(forKey: "hasSeenNewSoundBanner") store.removeObject(forKey: "hasSeenNewSoundBanner")
} }
@@ -65,6 +69,10 @@ final class UserPreferenceMacroTests: XCTestCase {
_liveLocationSharingSessionsByRoomIDSubject.prepend(liveLocationSharingSessionsByRoomID).eraseToAnyPublisher() _liveLocationSharingSessionsByRoomIDSubject.prepend(liveLocationSharingSessionsByRoomID).eraseToAnyPublisher()
} }
var liveLocationSharingSessionsByRoomIDKey: UserPreferenceKey {
.init("liveLocationSharingTimeoutDatesByRoomID")
}
func resetLiveLocationSharingSessionsByRoomID() { func resetLiveLocationSharingSessionsByRoomID() {
store.removeObject(forKey: "liveLocationSharingTimeoutDatesByRoomID") store.removeObject(forKey: "liveLocationSharingTimeoutDatesByRoomID")
} }
@@ -95,6 +103,10 @@ final class UserPreferenceMacroTests: XCTestCase {
_pusherProfileTagSubject.prepend(pusherProfileTag).eraseToAnyPublisher() _pusherProfileTagSubject.prepend(pusherProfileTag).eraseToAnyPublisher()
} }
var pusherProfileTagKey: UserPreferenceKey {
.init("pusherProfileTag")
}
func resetPusherProfileTag() { func resetPusherProfileTag() {
store.removeObject(forKey: "pusherProfileTag") store.removeObject(forKey: "pusherProfileTag")
} }
@@ -125,6 +137,10 @@ final class UserPreferenceMacroTests: XCTestCase {
_viewSourceEnabledSubject.prepend(viewSourceEnabled).eraseToAnyPublisher() _viewSourceEnabledSubject.prepend(viewSourceEnabled).eraseToAnyPublisher()
} }
var viewSourceEnabledKey: UserPreferenceKey {
.init("viewSourceEnabled")
}
func resetViewSourceEnabled() { func resetViewSourceEnabled() {
store.removeObject(forKey: "viewSourceEnabled") store.removeObject(forKey: "viewSourceEnabled")
} }
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0b1465b1e379f4356e0cbde2ecc1a9cd2a58c7a69be7ca9f7b3b2df2e911e60c
size 2004302
@@ -0,0 +1,19 @@
//
// Copyright 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.
//
import SwiftUI
/// Implemented by MapLibreShim's entry point, which the app instantiates after
/// `dlopen`-ing the framework.
public protocol MapLibreShimProtocol {
/// Routes MapLibre's logs into the app's logger. Call once after loading the shim.
func configureLogging(_ handler: @escaping @Sendable (MapLogSeverity, String) -> Void)
/// Builds the interactive map view. The app-side `MapLibreMapView` forwards every
/// body evaluation here so SwiftUI updates flow through to the underlying map.
func makeMapView(configuration: MapViewConfiguration) -> AnyView
}
@@ -0,0 +1,56 @@
//
// Copyright 2026 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.
//
import CoreLocation
import Foundation
/**
Behavior mode of the current user's location, can be hidden, only shown and shown following the user
*/
public enum ShowUserLocationMode: Equatable {
/// this mode will show the user pin in map
case show
/// this mode will show the user pin in map and track him, panning the map automatically
case showAndFollow
/// this mode will not show the user pin in map
case hide
/// this mode will not show the user pin in map and will follow the marker with the given id,
/// panning the map automatically.
case hideAndFollowMarker(id: String)
/// The id of the marker the map should follow when in the `hideAndFollowMarker` mode, nil otherwise.
public var followedMarkerID: String? {
guard case .hideAndFollowMarker(let id) = self else { return nil }
return id
}
}
public enum MapLibreError: Error, Hashable {
case failedLoadingMap
case failedLocatingUser
}
/// The severity of a map log message forwarded to the app.
public enum MapLogSeverity {
case error
case warning
case info
case debug
case verbose
}
/// A marker shown on the map, identified so that it can be moved in place when its coordinate changes.
public struct MapAnnotation {
public let id: String
public let coordinate: CLLocationCoordinate2D
public init(id: String, coordinate: CLLocationCoordinate2D) {
self.id = id
self.coordinate = coordinate
}
}
@@ -0,0 +1,76 @@
//
// Copyright 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.
//
import CoreLocation
import SwiftUI
/// Everything the map implementation needs in order to render, expressed without app
/// types so that the implementation framework can be loaded on demand.
public struct MapViewConfiguration {
/// the final zoom level used when the first user location emit
public let zoomLevel: Double
/// The initial zoom level used when the map it firstly loaded and the user location is not yet available, in case of annotations this property is not being used
public let initialZoomLevel: Double
/// The initial map center
public let mapCenter: CLLocationCoordinate2D
/// Map annotations
public let annotations: [MapAnnotation]
/// The style to load the map with, for either the dark or the light appearance.
public let styleURL: (_ isDark: Bool) -> URL?
public let tintColor: UIColor
/// The content of the marker representing the annotation with the given id.
public let markerView: (_ annotationID: String) -> AnyView?
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
public let showsUserLocationMode: Binding<ShowUserLocationMode>
/// Bind view errors if any
public let error: Binding<MapLibreError?>
/// Coordinate of the center of the map
public let mapCenterCoordinate: Binding<CLLocationCoordinate2D?>
public let hasLoadedUserLocation: Binding<Bool>
public let isLocationAuthorized: Binding<Bool?>
/// The radius of uncertainty for the location, measured in meters.
public let geolocationUncertainty: Binding<CLLocationAccuracy?>
/// Called when the user pan on the map
public let userDidPan: (() -> Void)?
public init(zoomLevel: Double,
initialZoomLevel: Double,
mapCenter: CLLocationCoordinate2D,
annotations: [MapAnnotation] = [],
styleURL: @escaping (_ isDark: Bool) -> URL?,
tintColor: UIColor,
markerView: @escaping (_ annotationID: String) -> AnyView?,
showsUserLocationMode: Binding<ShowUserLocationMode>,
error: Binding<MapLibreError?>,
mapCenterCoordinate: Binding<CLLocationCoordinate2D?>,
hasLoadedUserLocation: Binding<Bool>,
isLocationAuthorized: Binding<Bool?>,
geolocationUncertainty: Binding<CLLocationAccuracy?>,
userDidPan: (() -> Void)? = nil) {
self.zoomLevel = zoomLevel
self.initialZoomLevel = initialZoomLevel
self.mapCenter = mapCenter
self.annotations = annotations
self.styleURL = styleURL
self.tintColor = tintColor
self.markerView = markerView
self.showsUserLocationMode = showsUserLocationMode
self.error = error
self.mapCenterCoordinate = mapCenterCoordinate
self.hasLoadedUserLocation = hasLoadedUserLocation
self.isLocationAuthorized = isLocationAuthorized
self.geolocationUncertainty = geolocationUncertainty
self.userDidPan = userDidPan
}
}
@@ -0,0 +1,19 @@
# The types shared between the app and the MapLibreShim framework. The app links this
# (tiny, initialiser-free) framework instead of MapLibre, see MapLibreShim's target.yml.
name: MapLibreInterface
targets:
MapLibreInterface:
type: framework
platform: iOS
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).maplibre-interface
GENERATE_INFOPLIST_FILE: YES
SWIFT_VERSION: 6
SWIFT_APPROACHABLE_CONCURRENCY: YES
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
sources:
- path: ../Sources
@@ -8,7 +8,7 @@
import CoreLocation import CoreLocation
import QuartzCore import QuartzCore
/// Smoothly animates a `LocationAnnotation`'s coordinate from its current /// Smoothly animates a `MapLibreAnnotation`'s coordinate from its current
/// position to a new one using a `CADisplayLink` for frame-accurate updates. /// position to a new one using a `CADisplayLink` for frame-accurate updates.
/// ///
/// ## How it works /// ## How it works
@@ -26,7 +26,7 @@ import QuartzCore
/// cancels the previous one and begins from the annotation's current position. /// cancels the previous one and begins from the annotation's current position.
final class CoordinateAnimator { final class CoordinateAnimator {
private var displayLink: CADisplayLink? private var displayLink: CADisplayLink?
private weak var annotation: LocationAnnotation? private weak var annotation: MapLibreAnnotation?
private let startCoordinate: CLLocationCoordinate2D private let startCoordinate: CLLocationCoordinate2D
private let endCoordinate: CLLocationCoordinate2D private let endCoordinate: CLLocationCoordinate2D
private let duration: CFTimeInterval private let duration: CFTimeInterval
@@ -36,7 +36,7 @@ final class CoordinateAnimator {
/// while the display link is running. Keyed by annotation ID. /// while the display link is running. Keyed by annotation ID.
private static var activeAnimators: [String: CoordinateAnimator] = [:] private static var activeAnimators: [String: CoordinateAnimator] = [:]
private init(annotation: LocationAnnotation, private init(annotation: MapLibreAnnotation,
to end: CLLocationCoordinate2D, to end: CLLocationCoordinate2D,
duration: CFTimeInterval) { duration: CFTimeInterval) {
self.annotation = annotation self.annotation = annotation
@@ -48,7 +48,7 @@ final class CoordinateAnimator {
/// Starts animating the annotation's coordinate to `end` over `duration` seconds. /// Starts animating the annotation's coordinate to `end` over `duration` seconds.
/// If the annotation is already being animated, the in-flight animation is /// If the annotation is already being animated, the in-flight animation is
/// cancelled and a new one starts from the current position. /// cancelled and a new one starts from the current position.
static func animate(annotation: LocationAnnotation, static func animate(annotation: MapLibreAnnotation,
to end: CLLocationCoordinate2D, to end: CLLocationCoordinate2D,
duration: CFTimeInterval) { duration: CFTimeInterval) {
guard annotation.coordinate.latitude != end.latitude guard annotation.coordinate.latitude != end.latitude
@@ -0,0 +1,60 @@
//
// Copyright 2026 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.
//
import CoreLocation
import MapLibre
import MapLibreInterface
import SwiftUI
final class MapLibreAnnotation: NSObject, MLNAnnotation, Identifiable {
let id: String
// @objc dynamic is required to make animations work
@objc dynamic var coordinate: CLLocationCoordinate2D
// MARK: - Setup
init(_ annotation: MapAnnotation) {
id = annotation.id
coordinate = annotation.coordinate
super.init()
}
}
final class MapLibreAnnotationView: MLNUserLocationAnnotationView {
private var hostingController: UIHostingController<AnyView>?
// MARK: - Setup
override init(annotation: MLNAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier:
reuseIdentifier)
}
convenience init(annotation: MapLibreAnnotation, content: AnyView?) {
self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
let hostingController = UIHostingController(rootView: content ?? AnyView(EmptyView()))
self.hostingController = hostingController
let view: UIView = hostingController.view
view.backgroundColor = .clear
view.anchorPoint = .init(x: 0.5, y: 1.0)
addSubview(view)
view.bounds.size = view.intrinsicContentSize
}
func updateContent(_ content: AnyView?) {
hostingController?.rootView = content ?? AnyView(EmptyView())
if let hostedView = hostingController?.view {
hostedView.bounds.size = hostedView.intrinsicContentSize
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError()
}
}
@@ -0,0 +1,297 @@
//
// Copyright 2026 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.
//
import MapLibre
import MapLibreInterface
import SwiftUI
struct MapLibreMapView: UIViewRepresentable {
// MARK: - Properties
@Environment(\.colorScheme) private var colorScheme
let configuration: MapViewConfiguration
// MARK: - UIViewRepresentable
func makeUIView(context: Context) -> MLNMapView {
let mapView = makeMapView()
mapView.delegate = context.coordinator
setupMap(mapView: mapView)
return mapView
}
func updateUIView(_ mapView: MLNMapView, context: Context) {
// Don't set the same value twice. Otherwise, if there is an error loading the map, a loop
// is caused as the `error` binding being set, which triggers this update, which sets a
// new URL, which causes another error, and so it goes on round and round in a circle.
let dynamicMapURL = configuration.styleURL(colorScheme == .dark)
if mapView.styleURL != dynamicMapURL {
mapView.styleURL = dynamicMapURL
}
// If the center coordinate was updated externally (not by the map itself), move the map.
// Not applied while following a marker, where the camera is driven by the marker's position.
if configuration.showsUserLocationMode.wrappedValue.followedMarkerID == nil,
let newCenter = configuration.mapCenterCoordinate.wrappedValue,
newCenter.latitude != context.coordinator.lastReportedCenter?.latitude
|| newCenter.longitude != context.coordinator.lastReportedCenter?.longitude {
context.coordinator.lastReportedCenter = newCenter
mapView.setCenter(newCenter, animated: true)
}
// Update existing annotation views with fresh SwiftUI content.
// This handles the case where the annotation's view data changes after
// the annotation was initially placed (e.g. user avatar loads asynchronously).
updateAnnotations(in: mapView)
showUserLocation(in: mapView)
context.coordinator.updateMarkerFollowing(in: mapView, markerID: configuration.showsUserLocationMode.wrappedValue.followedMarkerID)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
// MARK: - Private
private func setupMap(mapView: MLNMapView) {
mapView.addAnnotations(configuration.annotations.map(MapLibreAnnotation.init))
mapView.zoomLevel = configuration.annotations.isEmpty ? configuration.initialZoomLevel : configuration.zoomLevel
mapView.centerCoordinate = configuration.mapCenter
}
private func updateAnnotations(in mapView: MLNMapView) {
let existingByID = Dictionary(uniqueKeysWithValues:
(mapView.annotations ?? []).compactMap { $0 as? MapLibreAnnotation }.map { ($0.id, $0) })
let updatedByID = Dictionary(uniqueKeysWithValues: configuration.annotations.map { ($0.id, $0) })
let existingIDs = Set(existingByID.keys)
let updatedIDs = Set(updatedByID.keys)
// Remove annotations that are no longer present
let removedIDs = existingIDs.subtracting(updatedIDs)
if !removedIDs.isEmpty {
let toRemove = removedIDs.compactMap { existingByID[$0] }
mapView.removeAnnotations(toRemove)
}
// Add new annotations
let addedIDs = updatedIDs.subtracting(existingIDs)
if !addedIDs.isEmpty {
let toAdd = addedIDs.compactMap { updatedByID[$0] }.map(MapLibreAnnotation.init)
mapView.addAnnotations(toAdd)
}
// Update existing annotations that are still present
let keptIDs = existingIDs.intersection(updatedIDs)
for id in keptIDs {
guard let existingAnnotation = existingByID[id],
let updatedAnnotation = updatedByID[id] else {
continue
}
CoordinateAnimator.animate(annotation: existingAnnotation,
to: updatedAnnotation.coordinate,
duration: 1.0)
if let annotationView = mapView.view(for: existingAnnotation) as? MapLibreAnnotationView {
annotationView.updateContent(configuration.markerView(id))
}
}
}
private func makeMapView() -> MLNMapView {
let mapView = MLNMapView(frame: .zero, styleURL: configuration.styleURL(colorScheme == .dark))
mapView.logoViewPosition = .topLeft
mapView.attributionButtonPosition = .topLeft
mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2)
mapView.tintColor = configuration.tintColor
mapView.allowsRotating = false
mapView.allowsTilting = false
return mapView
}
private func showUserLocation(in mapView: MLNMapView) {
switch (configuration.showsUserLocationMode.wrappedValue, configuration.annotations) {
case (.showAndFollow, _):
mapView.userTrackingMode = .follow
case (.show, let annotations) where !annotations.isEmpty:
// In the show mode, if there are annotations, we check the authorizationStatus,
// if it's not determined, we wont prompt the user with a request for permissions,
// because they should be able to see the annotations without sharing their location information.
guard mapView.locationManager.authorizationStatus != .notDetermined else { return }
fallthrough
case (.show, _):
mapView.showsUserLocation = true
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
case (.hide, _), (.hideAndFollowMarker, _):
// In hideAndFollowMarker mode the camera following is handled by the coordinator.
mapView.showsUserLocation = false
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
}
}
}
// MARK: - Coordinator
extension MapLibreMapView {
class Coordinator: NSObject, MLNMapViewDelegate {
// MARK: - Properties
var mapLibreView: MapLibreMapView
private var previousUserLocation: MLNUserLocation?
/// Tracks the last center coordinate reported by the map (or set programmatically),
/// so that `updateUIView` can tell apart external binding changes from internal ones.
var lastReportedCenter: CLLocationCoordinate2D?
/// The annotation the camera is currently locked on while in the `hideAndFollowMarker` mode.
private weak var followedAnnotation: MapLibreAnnotation?
/// Observes the followed annotation's coordinate, updated every frame by the `CoordinateAnimator`.
private var followedAnnotationObservation: NSKeyValueObservation?
// MARK: - Setup
init(_ mapLibreView: MapLibreMapView) {
self.mapLibreView = mapLibreView
}
// MARK: - Marker following
/// Keeps the camera locked on the marker with the given id, or stops following when nil.
func updateMarkerFollowing(in mapView: MLNMapView, markerID: String?) {
guard let markerID else {
stopFollowingMarker()
return
}
guard let annotation = (mapView.annotations ?? [])
.compactMap({ $0 as? MapLibreAnnotation })
.first(where: { $0.id == markerID }) else {
// The marker isn't on the map (yet), resolve it again on the next update.
stopFollowingMarker()
return
}
guard annotation !== followedAnnotation else { return }
startFollowingMarker(annotation, in: mapView)
}
private func startFollowingMarker(_ annotation: MapLibreAnnotation, in mapView: MLNMapView) {
followedAnnotation = annotation
followedAnnotationObservation = nil
// Animate to the marker first, attaching the per-frame tracking only on completion
// so that it doesn't cancel the transition.
mapView.setCenter(annotation.coordinate,
zoomLevel: mapView.zoomLevel,
direction: -1, // negative value keeps the current direction
animated: true) { [weak self, weak annotation, weak mapView] in
guard let self, let annotation, let mapView, annotation === followedAnnotation else { return }
// Mirror every animated coordinate update to keep the camera as smooth as the marker.
followedAnnotationObservation = annotation.observe(\.coordinate) { [weak mapView] annotation, _ in
// The coordinate is only ever updated by SwiftUI so KVO fires on the main actor.
MainActor.assumeIsolated {
mapView?.setCenter(annotation.coordinate, animated: false)
}
}
}
}
private func stopFollowingMarker() {
followedAnnotation = nil
followedAnnotationObservation = nil
}
// MARK: - MLNMapViewDelegate
func mapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? {
guard let annotation = annotation as? MapLibreAnnotation else {
return nil
}
return MapLibreAnnotationView(annotation: annotation, content: mapLibreView.configuration.markerView(annotation.id))
}
func mapViewDidFailLoadingMap(_ mapView: MLNMapView, withError error: Error) {
if mapLibreView.configuration.error.wrappedValue != .failedLoadingMap {
mapLibreView.configuration.error.wrappedValue = .failedLoadingMap
}
}
func mapView(_ mapView: MLNMapView, didUpdate userLocation: MLNUserLocation?) {
guard let userLocation else { return }
mapLibreView.configuration.hasLoadedUserLocation.wrappedValue = true
if previousUserLocation == nil, mapLibreView.configuration.annotations.isEmpty {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.configuration.zoomLevel, animated: true)
}
}
previousUserLocation = userLocation
updateGeolocationUncertainty(location: userLocation)
}
func mapView(_ mapView: MLNMapView, didChangeLocationManagerAuthorization manager: MLNLocationManager) {
switch manager.authorizationStatus {
case .denied, .restricted:
mapLibreView.configuration.isLocationAuthorized.wrappedValue = false
case .authorizedAlways, .authorizedWhenInUse:
mapLibreView.configuration.isLocationAuthorized.wrappedValue = true
case .notDetermined:
mapLibreView.configuration.isLocationAuthorized.wrappedValue = nil
@unknown default:
break
}
}
func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) {
// While following a marker, don't flood the center binding with per-frame camera updates.
// Leaving `lastReportedCenter` untouched avoids an external re-centering when following stops.
guard mapLibreView.configuration.showsUserLocationMode.wrappedValue.followedMarkerID == nil else { return }
// Avoid `Publishing changes from within view update` warnings
DispatchQueue.main.async { [mapLibreView, weak self] in
let center = mapView.centerCoordinate
self?.lastReportedCenter = center
mapLibreView.configuration.mapCenterCoordinate.wrappedValue = center
}
}
func mapView(_ mapView: MLNMapView, shouldChangeFrom oldCamera: MLNMapCamera, to newCamera: MLNMapCamera, reason: MLNCameraChangeReason) -> Bool {
// Send userDidPan only for gestures that actually change the map center. The reason
// is an option set and gestures can come combined with other reasons, so check for
// containment instead of an exact match.
let centerChangingGestures: MLNCameraChangeReason = [.gesturePan, .gesturePinch, .gestureRotate]
if !reason.isDisjoint(with: centerChangingGestures) {
// Stop following immediately, the camera would fight the gesture otherwise.
stopFollowingMarker()
mapLibreView.configuration.userDidPan?()
}
return true
}
// MARK: Callout
func mapView(_ mapView: MLNMapView, annotationCanShowCallout annotation: MLNAnnotation) -> Bool {
false
}
// MARK: Private
private func updateGeolocationUncertainty(location: MLNUserLocation) {
guard let clLocation = location.location, clLocation.horizontalAccuracy >= 0 else {
mapLibreView.configuration.geolocationUncertainty.wrappedValue = nil
return
}
mapLibreView.configuration.geolocationUncertainty.wrappedValue = clLocation.horizontalAccuracy
}
}
}
@@ -0,0 +1,39 @@
//
// Copyright 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.
//
import MapLibre
import MapLibreInterface
import SwiftUI
/// The shim's entry point: the app looks this class up by name through the ObjC runtime
/// after `dlopen`-ing the framework, keeping MapLibre off the app's launch path.
@objc(MapLibreShim)
public final class MapLibreShim: NSObject, MapLibreShimProtocol {
public func configureLogging(_ handler: @escaping @Sendable (MapLogSeverity, String) -> Void) {
MLNLoggingConfiguration.shared.loggingLevel = .debug
MLNLoggingConfiguration.shared.handler = { loggingLevel, _, _, message in
switch loggingLevel {
case .error:
handler(.error, message)
case .warning:
handler(.warning, message)
case .info:
handler(.info, message)
case .debug:
handler(.debug, message)
case .verbose:
handler(.verbose, message)
default:
break
}
}
}
public func makeMapView(configuration: MapViewConfiguration) -> AnyView {
AnyView(MapLibreMapView(configuration: configuration))
}
}
@@ -0,0 +1,26 @@
# The interactive map, split out of the app so MapLibre's static initialisers stay off the
# launch path: the app links only the tiny MapLibreInterface framework, while MapLibreShim (the
# only image linking MapLibre) is embedded unlinked and dlopen'd on first map use.
name: MapLibreShim
targets:
MapLibreShim:
type: framework
platform: iOS
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).maplibre-shim
GENERATE_INFOPLIST_FILE: YES
SWIFT_VERSION: 6
SWIFT_APPROACHABLE_CONCURRENCY: YES
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
dependencies:
- target: MapLibreInterface
embed: false
- package: MapLibre
embed: false
sources:
- path: ../Sources
+9
View File
@@ -0,0 +1,9 @@
nonisolated enum Secrets {
static let sentryDSN: String? = nil
static let sentryRustDSN: String? = nil
static let postHogHost: String? = nil
static let postHogAPIKey: String? = nil
static let rageshakeURL: String? = nil
static let mapLibreAPIKey: String? = "your_key"
}

Before

Width:  |  Height:  |  Size: 387 KiB

After

Width:  |  Height:  |  Size: 387 KiB

Before

Width:  |  Height:  |  Size: 572 B

After

Width:  |  Height:  |  Size: 572 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 818 B

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

-52
View File
@@ -1,52 +0,0 @@
import Danger
import Foundation
SwiftLint.lint(.modifiedAndCreatedFiles(directory: nil),
inline: true,
configFile: nil,
strict: false,
quiet: true,
swiftlintPath: nil,
markdownAction: { _ in })
let danger = Danger()
/// All of the new and modified files together.
let editedFiles = danger.git.modifiedFiles + danger.git.createdFiles
// Warn when there is a big PR
if (danger.github.pullRequest.additions ?? 0) > 1000 {
warn("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
}
// Check that the PR has a description
if danger.github.pullRequest.body?.isEmpty ?? true {
warn("Please provide a description for this PR.")
}
/// Check for screenshots on view changes
let hasChangedViews = editedFiles.contains { $0.lowercased().contains("/view") }
if hasChangedViews {
if (danger.github.pullRequest.body?.contains("user-attachments") ?? false) == false {
warn("You seem to have made changes to views. Please consider adding screenshots.")
}
}
/// Check for pngs on resources
let hasPngs = editedFiles.contains { $0.lowercased().contains(".xcassets") && $0.lowercased().hasSuffix(".png") }
if hasPngs {
warn("You seem to have made changes to some resource images. Please consider using an SVG or PDF.")
}
// Check for nice PR titles
let prTitle = danger.github.pullRequest.title
let fixesRegex = try! Regex("(Fixes|Fix) #\\d+")
let semanticRegex = try! Regex("\\w+\\(\\w+\\):")
if prTitle.hasSuffix("…") || prTitle.starts(with: fixesRegex) || prTitle.starts(with: semanticRegex) {
fail("Please provide a complete title that can be used as a changelog entry.")
}
// Check for changelog tags
if danger.github.issue.labels.filter({ $0.name.hasPrefix("pr-") }).count != 1 {
fail("Please add a `pr-` label to categorise the changelog entry.")
}
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc39671ee91a08779e4149589a4635fa6b91519b07bfb2dee621ccab031a75c5
size 5825201
File diff suppressed because it is too large Load Diff
@@ -1,13 +1,13 @@
{ {
"originHash" : "1c7d307d80ee561100a0432a5ae13f345392a1175c7f6c17c2eb0da269382c57", "originHash" : "fbdb76ae8c78e5a86db4812456fcb4da5795eed4bc8f6715726fec41e87a1e0f",
"pins" : [ "pins" : [
{ {
"identity" : "compound-design-tokens", "identity" : "compound-design-tokens",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/compound-design-tokens", "location" : "https://github.com/element-hq/compound-design-tokens",
"state" : { "state" : {
"revision" : "6e33c5bf3cbb4c29dbe5fc063d24b375849c0fac", "revision" : "28b921601ac0ff49a572b6f18526d97bd14e5d91",
"version" : "10.2.2" "version" : "11.3.1"
} }
}, },
{ {
@@ -15,8 +15,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/devicekit/DeviceKit", "location" : "https://github.com/devicekit/DeviceKit",
"state" : { "state" : {
"revision" : "56b997e8a61707218f9af09f32b2a1d1806fd792", "revision" : "19528aa07e1426626ea9fb2c4a161edf43c4f9e8",
"version" : "5.8.0" "version" : "5.9.0"
} }
}, },
{ {
@@ -55,13 +55,22 @@
"version" : "1.2.0" "version" : "1.2.0"
} }
}, },
{
"identity" : "element-call-ios",
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/element-call-ios",
"state" : {
"revision" : "d77b4ee326582083cabe53ffbd872a751c3cb989",
"version" : "0.1.0-rc.11"
}
},
{ {
"identity" : "element-call-swift", "identity" : "element-call-swift",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/element-call-swift", "location" : "https://github.com/element-hq/element-call-swift",
"state" : { "state" : {
"revision" : "d2ecef5a54e5718ed6344dde6f72a4da42f1fcd9", "revision" : "befb5d37a7859ad38cf83719cbfd4abaffa04d82",
"version" : "0.20.2" "version" : "0.26.1"
} }
}, },
{ {
@@ -95,8 +104,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher", "location" : "https://github.com/onevcat/Kingfisher",
"state" : { "state" : {
"revision" : "ac632bd26a1c00f139ff62fd01806f21cf67325e", "revision" : "8648fe649c5d62be38721110f835821906e9242a",
"version" : "8.10.0" "version" : "8.13.0"
} }
}, },
{ {
@@ -122,8 +131,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/LRUCache", "location" : "https://github.com/nicklockwood/LRUCache",
"state" : { "state" : {
"revision" : "cb5b2bd0da83ad29c0bec762d39f41c8ad0eaf3e", "revision" : "f0570e943ea4445d258113ba73f50cb2b7e564ac",
"version" : "1.2.1" "version" : "1.3.0"
} }
}, },
{ {
@@ -131,8 +140,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/guoyingtao/Mantis", "location" : "https://github.com/guoyingtao/Mantis",
"state" : { "state" : {
"revision" : "b3c237aff54b8307440157a2f5956bdea2e76791", "revision" : "0960be1feed2e62f62a5fddbb8d72b26008db881",
"version" : "2.31.2" "version" : "3.1.0"
} }
}, },
{ {
@@ -140,8 +149,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution", "location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
"state" : { "state" : {
"revision" : "84a79bc375a301169390ac110c868f06c857b83f", "revision" : "13e41ab3d77ff5113e7e5d4ee87803b1f81f5683",
"version" : "6.27.0" "version" : "6.31.0"
} }
}, },
{ {
@@ -149,8 +158,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-analytics-events", "location" : "https://github.com/matrix-org/matrix-analytics-events",
"state" : { "state" : {
"revision" : "4b143b14a16a231bd4fc3c6705845f852177abe3", "revision" : "7d668b3e4be493843805b8a1ff4e13b261a19965",
"version" : "0.37.0" "version" : "0.38.0"
} }
}, },
{ {
@@ -158,8 +167,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rich-text-editor-swift", "location" : "https://github.com/element-hq/matrix-rich-text-editor-swift",
"state" : { "state" : {
"revision" : "c0877fb9323d1ec2de56e4fe2da97f73cac36c38", "revision" : "56077711bf495db926ec3f3efd36ed127fb9ceb1",
"version" : "2.42.0" "version" : "2.42.1"
} }
}, },
{ {
@@ -167,8 +176,17 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift", "location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "24c26f6697e03e42d176c61b5923ac958f5fa32d", "revision" : "dbdf59e13d0eade7fd9cbd01cfd564693b85b761",
"version" : "26.7.3" "version" : "26.10.15"
}
},
{
"identity" : "matrix-rust-rtc",
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-rtc",
"state" : {
"revision" : "c7a81b6742dc5e571b90b05f5109677384efb933",
"version" : "0.5.0-rc.5"
} }
}, },
{ {
@@ -194,8 +212,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/PostHog/posthog-ios", "location" : "https://github.com/PostHog/posthog-ios",
"state" : { "state" : {
"revision" : "6e70aa24bbd3778182fe039f7f5800a17674a6d6", "revision" : "d97d9a0babda42ecee026bd0a5a9ea1756349dc1",
"version" : "3.61.1" "version" : "3.85.3"
} }
}, },
{ {
@@ -203,8 +221,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa", "location" : "https://github.com/getsentry/sentry-cocoa",
"state" : { "state" : {
"revision" : "43abfbf9a26089ad34604c718ad0935440caf1d4", "revision" : "8689e780a295dfdc6501ac0dbb502461e16d6551",
"version" : "9.18.0" "version" : "9.29.2"
} }
}, },
{ {
@@ -230,17 +248,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms", "location" : "https://github.com/apple/swift-async-algorithms",
"state" : { "state" : {
"revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440", "revision" : "789dcf1f3d3f00251482f40a432ab7144c181987",
"version" : "1.1.4" "version" : "1.1.6"
}
},
{
"identity" : "swift-clocks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "72d749bf341b78851203066ab421869b783ec42a",
"version" : "1.1.0"
} }
}, },
{ {
@@ -248,17 +257,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections", "location" : "https://github.com/apple/swift-collections",
"state" : { "state" : {
"revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", "revision" : "98ef3c98609a1e31b7e157b5b619579001a789d6",
"version" : "1.6.0" "version" : "1.7.1"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
"version" : "1.3.2"
} }
}, },
{ {
@@ -293,8 +293,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing", "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : { "state" : {
"revision" : "ad5e3190cc63dc288f28546f9c6827efc1e9d495", "revision" : "28e5de025e3fd98991791bfbf83023ab3708da03",
"version" : "1.19.2" "version" : "1.19.6"
} }
}, },
{ {
@@ -302,8 +302,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax", "location" : "https://github.com/swiftlang/swift-syntax",
"state" : { "state" : {
"revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1", "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
"version" : "603.0.2" "version" : "604.0.0"
} }
}, },
{ {
@@ -311,8 +311,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git", "location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : { "state" : {
"revision" : "49dcadd93161f4a44b4994d3a3e8de9f085aface", "revision" : "18b80329749eca5ea29fc50211dca5c7eff5bfec",
"version" : "2.13.5" "version" : "2.13.9"
} }
}, },
{ {
@@ -329,8 +329,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/tevelee/SwiftUI-Flow.git", "location" : "https://github.com/tevelee/SwiftUI-Flow.git",
"state" : { "state" : {
"revision" : "798364823cdfc6afb4aff43eff5a3ddbca89b54f", "revision" : "19e86b750b3c67260cbb28c459004c0dfdb11899",
"version" : "3.4.0" "version" : "3.5.1"
} }
}, },
{ {
@@ -338,8 +338,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect", "location" : "https://github.com/siteline/SwiftUI-Introspect",
"state" : { "state" : {
"revision" : "26986a57e31c813f98262ddb62d4b07d40008535", "revision" : "a37eca1db7bebc9437ee89508e7b4433ff34adb4",
"version" : "26.0.1" "version" : "27.0.0"
} }
}, },
{ {
@@ -4,8 +4,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -27,14 +26,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
onlyGenerateCoverageForSpecifiedTargets = "NO">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:AccessibilityTests/SupportingFiles/AccessibilityTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -44,10 +36,12 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables> <TestPlans>
</Testables> <TestPlanReference
<CommandLineArguments> reference = "container:AccessibilityTests/SupportingFiles/AccessibilityTests.xctestplan"
</CommandLineArguments> default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -68,8 +62,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -77,8 +69,6 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<CommandLineArguments>
</CommandLineArguments>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -4,8 +4,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES" codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES"> onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets> <CodeCoverageTargets>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</CodeCoverageTargets> </CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -79,14 +74,17 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
<EnvironmentVariables> <EnvironmentVariables>
<EnvironmentVariable <EnvironmentVariable
key = "HTTPS_PROXY" key = "HTTPS_PROXY"
value = "localhost:9090" value = "localhost:9090"
isEnabled = "NO"> isEnabled = "NO">
</EnvironmentVariable> </EnvironmentVariable>
<EnvironmentVariable
key = "HTTP_PROXY"
value = "localhost:9090"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable <EnvironmentVariable
key = "UI_TESTS_SCREEN" key = "UI_TESTS_SCREEN"
value = "" value = ""
@@ -115,8 +113,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">
@@ -4,8 +4,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES" codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES"> onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:IntegrationTests/SupportingFiles/IntegrationTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets> <CodeCoverageTargets>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</CodeCoverageTargets> </CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:IntegrationTests/SupportingFiles/IntegrationTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -78,8 +73,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -87,8 +80,6 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<CommandLineArguments>
</CommandLineArguments>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -5,8 +5,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -28,8 +27,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -41,8 +39,6 @@
</MacroExpansion> </MacroExpansion>
<Testables> <Testables>
</Testables> </Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -66,8 +62,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -85,8 +79,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">
@@ -4,8 +4,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES" codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES"> onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:PreviewTests/SupportingFiles/PreviewTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets> <CodeCoverageTargets>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</CodeCoverageTargets> </CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:PreviewTests/SupportingFiles/PreviewTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -78,8 +73,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -87,8 +80,6 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<CommandLineArguments>
</CommandLineArguments>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -5,8 +5,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -28,8 +27,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -41,8 +39,6 @@
</MacroExpansion> </MacroExpansion>
<Testables> <Testables>
</Testables> </Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -66,8 +62,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -85,8 +79,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction> </ProfileAction>
<AnalyzeAction <AnalyzeAction
buildConfiguration = "Debug"> buildConfiguration = "Debug">
@@ -4,8 +4,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -27,14 +26,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
onlyGenerateCoverageForSpecifiedTargets = "NO">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UITests/SupportingFiles/UITests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -44,10 +36,12 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables> <TestPlans>
</Testables> <TestPlanReference
<CommandLineArguments> reference = "container:UITests/SupportingFiles/UITests.xctestplan"
</CommandLineArguments> default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -68,8 +62,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -77,8 +69,6 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<CommandLineArguments>
</CommandLineArguments>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -4,8 +4,7 @@
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES">
runPostActionsOnFailure = "NO">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -30,12 +29,6 @@
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES" codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES"> onlyGenerateCoverageForSpecifiedTargets = "YES">
<TestPlans>
<TestPlanReference
default = "YES"
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan">
</TestPlanReference>
</TestPlans>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -45,10 +38,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<Testables>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
<CodeCoverageTargets> <CodeCoverageTargets>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
@@ -58,6 +47,12 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</CodeCoverageTargets> </CodeCoverageTargets>
<TestPlans>
<TestPlanReference
reference = "container:UnitTests/SupportingFiles/UnitTests.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -78,8 +73,6 @@
ReferencedContainer = "container:ElementX.xcodeproj"> ReferencedContainer = "container:ElementX.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"
@@ -87,8 +80,6 @@
savedToolIdentifier = "" savedToolIdentifier = ""
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"> debugDocumentVersioning = "YES">
<CommandLineArguments>
</CommandLineArguments>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 265 KiB

@@ -1,15 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "app-logo.pdf", "filename" : "app-logo.png",
"idiom" : "universal" "idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "app-logo@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "app-logo@3x.png",
"idiom" : "universal",
"scale" : "3x"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"preserves-vector-representation" : true
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

@@ -0,0 +1,6 @@
"NSCameraUsageDescription" = "يحتاج Element X إلى الوصول إلى الكاميرا لالتقاط الصور أو مقاطع الفيديو وإرسالها كرسائل.";
"NSFaceIDUsageDescription" = "يُستخدم Face ID لحماية الوصول إلى حسابك.";
"NSLocationAlwaysAndWhenInUseUsageDescription" = "لمشاركة موقعك المباشر، يحتاج Element X إلى الوصول إلى الموقع عندما يكون التطبيق في الخلفية.";
"NSLocationWhenInUseUsageDescription" = "امنح الوصول إلى الموقع حتى يتمكن Element X من مشاركة موقعك.";
"NSMicrophoneUsageDescription" = "يحتاج Element X إلى الوصول إلى الميكروفون لتسجيل الرسائل الصوتية وإرسالها.";
"NSPhotoLibraryUsageDescription" = "يتيح لك هذا حفظ الصور ومقاطع الفيديو في مكتبة الصور لديك.";

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