* 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
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.
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>
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.
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.
* Fix some isolation errors when compiling with Swift 6.4.
* Update the Design Tokens dependency to include Sendable conformances.
(cherry picked from commit 4608a63e29)
* 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
* 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
* 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
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
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.
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.
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.
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
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
* 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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>
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>
* 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.
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
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.
* 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.
* 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.
* 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>
* 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.
* 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>
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.
* 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.
* 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.
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.
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.
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.
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.
* Remove the dynamic member lookup from UserSettings in favour of nested access.
* Remove forwarding methods from UserSettings.
* Update the Enterprise submodule.
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.
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.
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.
* 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.
* 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..
* 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.
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.
* 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.
* 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).
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.
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.
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.
* 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>
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.
* 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>
* 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.
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.
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.
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.
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.
`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.
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.
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
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.
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.
UIWindow.init() is deprecated in iOS 26. AuthenticationStartScreenViewModelTests
now creates its fake window from the test host's connected scene, matching
ServerSelectionScreenViewModelTests.
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.
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.
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.
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.
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.
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.
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.
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.
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.
* 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.
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
* 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.
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.
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.
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.
* 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.
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.
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.
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.
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.
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.
* 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.
* 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>
* 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.
* 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.
* 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>
* 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.
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.
* 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>
* 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.
* 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.
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.
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.
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.
* 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.
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.
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.
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.
* 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.
- 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.
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.
- 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).
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.
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.
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.
Continue grouping the standalone modules under Components. Updates the
preview asset paths and the media resource references in the app and unit
test targets.
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.
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.
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.
* 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>
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.
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.
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.
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.
* 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
* 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>
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.
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.
* 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>
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.
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.
* 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>
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>
* 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
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.
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
* 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>
* 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>
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>
* 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>
* 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
* 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.
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.
* 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.
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.
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>
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>
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.
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.
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.
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.
* 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.
* 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
3490 changed files with 51869 additions and 18958 deletions
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.
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
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 |
| 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:
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:
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.
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`.
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-label:"Stale"
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."
# 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."
# 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
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:
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
group:${{ format('unit-tests-{0}', github.ref) }}
cancel-in-progress:true
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.
@@ -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**.
- 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`.
- **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
@@ -52,10 +55,12 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
## Pull Request Guidelines
- New branch from `develop` must not track `develop`.
- Sentence-style titles (no conventional commits).
- Exactly one `pr-` label (see `.github/release.yml`).
- Title = changelog entry — descriptive, no "Fixes #…".
- 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.
- 500 additions max — split big changes.
- 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
### Build System
@@ -269,13 +285,16 @@ Services = where product-level opinions live (Rust SDK stays spec-faithful).
- Inject via `init` parameters.
- Screen coordinators get `Parameters` struct with specific dependencies.
-`CommonFlowParameters` flow-coordinator-only.
-`ServiceLocator`**deprecated** — never use services directly. Always inject from above.
- 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
- 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.
- Services + data-layer types: `nonisolated` + `Sendable` when background work need.
- 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. |
* 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
* 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
* 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
* 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
* 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
* 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
@@ -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)
* 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
* 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
* 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
@@ -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.
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
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).
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.
"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
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.