* 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.
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.
* 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
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.
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.
* 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
* 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.
* 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>
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.
* 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 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).
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.
* 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.
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.
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.