homeserverReachabilityPublisher now reports unreachable while suspended so media loading parks instead of failing against a closed store. The reachability banner was keyed off the same publisher, so pausing the client would surface a spurious "Server unreachable" message.
Expose a separate homeserverConnectivityPublisher derived from the sync service state alone (it stays reachable while suspended) and key the banner off that, keeping the suspend-aware publisher for gating work that needs an open store.
`homeserverReachabilityPublisher` was driven directly by the sync service state so stopping sync on pause still reported .reachable. MediaProvider.loadImageRetryingOnReconnection would then retry against a closed store, fail, and bail out permanently because it treated the reachable state as a non-recoverable error.
Make reachability a function of both the desired service state and the latest sync state, computed in one place: store the sync state in the listener and recompute reachability from it, reporting unreachable whenever the client isn't meant to be running. Recompute on suspend and on resume.
The field relied on its placeholder for a label, which VoiceOver only reads
while empty. Set a persistent accessibility label so it's always announced.
Addresses #5256.
The address field already had an accessibility label, but the section's
helper text and validation error were only shown in a separate footer and
weren't conveyed to VoiceOver when focusing the field. Compose them into the
field's accessibility hint.
Addresses #5258.
scrollToOldestItem targeted (timelineItemsIDs.count - 1, section: 1) even
when the table's data source hadn't applied that section yet, raising an
out-of-bounds NSRangeException. Scroll against the table's actual section
and row counts instead.
AudioRecorder.startRecording crashed AVAudioEngine
(IsFormatSampleRateAndChannelCountValid) when the input node reported an
invalid format - e.g. a zero sample rate when no input route is
available. Bail out with an error before connecting nodes or installing
the tap.
Both PhotoLibraryPicker and DocumentPicker reported failures via .error
but still emitted .selectedMediaAtURLs([]) when every selected item
failed to load or export (e.g. an iCloud photo that isn't downloaded, or
a document that fails the security-scoped temp copy) - an empty "success"
that consumers had to defend against. Skip that emission in both, and
report .error for PhotoLibraryPicker's previously silent
missing-content-type case so the user gets feedback.
LoadableImageContent.data(handler:) used MainActor.assumeIsolated to read
the loader's content, which crashes (libdispatch precondition) when
Kingfisher invokes the provider off the main thread, and fatalError'd
when the content wasn't GIF data. Hop onto the main actor and return a
failure instead. The handler is @Sendable per Kingfisher's protocol.
NotificationToneManager.init force-crashed (fatalError) when
createDirectory failed, e.g. a sandbox permission error (NSCocoaError
513). Log the error instead of killing the app. The underlying cause is
acknowledged and will be treated separately.
cmd+k jumps to the search tab but left users with no way back. Pressing
escape in the search field now returns to the tab they came from (never
into search itself).
Escape is handled at the search field's pressesBegan, alongside the
existing arrow-key navigation, since the focused UIKit field swallows
SwiftUI keyboard shortcuts.
Sentry's beforeSend runs on a background queue, but Event.crashLog read
UIDevice.current there, which carries a main-thread dispatch assertion and
trapped libdispatch. Read the device values on the main thread at setup and
pass them into the now-nonisolated crashLog.
The address field already had an accessibility label, but the section's
helper text and validation error were only shown in a separate footer and
weren't conveyed to VoiceOver when focusing the field. Compose them into the
field's accessibility hint.
Addresses #5258.
* Added a chevron to collapsed call buttons in the DM
This makes the context menu more intuitive and easy to find. Inspired by the collapsed state of call buttons in Whatsapp that I saw recently
* Shortening video/voice call button strings
Currently the text is unnecessarily long, I have reduced to button text
# Conflicts:
# ElementX/Sources/Generated/Strings+Untranslated.swift
* Move translations to localazy
---------
Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
The QLPreviewItem used the media's file name as its title, which VoiceOver
read as cryptic alt text. Provide a descriptive "Photo preview" / "Video
preview" title based on the media type instead.
Addresses #5240, #5241.
The media upload preview hides the QLPreviewController's navigation bar and
toolbar share button visually, but their buttons stayed in VoiceOver's focus
order. Mark the chrome as accessibility-hidden so it is no longer focusable.
Addresses #5266, #5267.
* Add a developer flag for the search tab
Introduces a globalSearchEnabled user preference (off by default) with a toggle in Developer Options, used to gate the new search experience.
* Support search role tabs in the tab coordinator
Migrates the NavigationTabCoordinator to SwiftUI's value-based Tab API and adds an isSearch option so a tab can adopt the iOS 26 .search role, which detaches it into the trailing search button in the tab bar.
* Moves room search to a new screen
Adds a new screen that filters the alternate room summary provider as you type, shows an empty state, and lists matching rooms with their matrix identifier. Supports hardware keyboard selection (arrow keys to move through results, return to open) by reaching into the system search field.
* Present search in a dedicated tab
Adds the search tab to the chats flow on iOS 26 when the flag is enabled, rendered as the detached .search role button. The Cmd+K shortcut now selects this tab, and the home screen's inline search is hidden while the tab is available.
* Remove the old global search screen
The dedicated search tab replaces the Cmd+K floating window, so the GlobalSearchScreen, its window plumbing in the WindowManager, and its presentation in the chats flow are all removed.
* add a11y label to the bug report field
* include a11y label to the list row directly
* remove is header trait from poll form question header section
* a11y label added to the create room screen name textfield
* Enable Swift 6.2 with default main actor isolation for the app target
Turn on SWIFT_VERSION 6.2, SWIFT_APPROACHABLE_CONCURRENCY and SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor for the ElementX target, matching the NSE and Share Extension (which already use 6.2 without the main actor default).
# Conflicts:
# ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
* Update the code generation templates for Swift 6
Emit nonisolated/@unchecked Sendable mocks and nonisolated string enums, and mark async mock methods @concurrent so they keep suspending like the real implementations (otherwise intermediate states are unobservable in tests). Includes the regenerated mocks and strings.
# Conflicts:
# ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
* Make the data layer and services nonisolated and Sendable
Annotate the value-type models, Rust SDK proxy wrappers, timeline item graph, settings, logging and NSE-shared types as nonisolated, and conform the service protocols to Sendable so their existentials can cross isolation boundaries. These types run off the main actor and no longer inherit the target's default main actor isolation.
* Adopt the new isolation across call sites, mocks and signatures
Update call sites, closure signatures and mock configuration helpers to match the nonisolated/Sendable data layer, including the main actor isolated mock inits and the @Sendable filter on recentlyVisitedRooms.
^ Conflicts:
^ ElementX/Sources/Services/SecureBackup/SecureBackupController.swift
* Remove redundant @MainActor annotations
With the app target defaulting to main actor isolation, the explicit @MainActor on view models, coordinators, views and their protocols is no longer required.
* Fix concurrency hotspots surfaced by the migration
Resolve the room summary main-thread deadlock by moving diff processing onto AsyncStreams, add the SDKListener.onMainActor FIFO bridge for SDK callbacks, hop KVO/timer/UIKit callbacks to the main actor via assumeIsolated, add isolated deinits, and apply the few justified nonisolated(unsafe) escapes for queue-confined Apple APIs.
* Fix the unit tests for Swift 6 concurrency
Hop the waitForConfirmation body onto the caller's actor (Swift Testing runs confirmation closures off-actor), and adjust the affected suites and async signatures.
* offload diffs on concurrent threads instead of main actor
* make room info protocols sendable
* use a task group for fetching data so that everything runs concurrently in the TextRoomTimelineView when getting the preview.
* use the generic onMainActor implementation, which already makes a stream and send the continuation on the main actor.
* small pr comments issues being addressed
* update enterprise
* Update StaticCode to 1.1.0 and specify the required Swift isolation.
* fix ElementCallServiceTests
* update enterprise
* re-generate mocks
* update enterprise
* enterprise new code conformed to swift 6
---------
Co-authored-by: Doug <douglase@element.io>
update generateRecoveryKey to take a passphrase option
Wire up AppHooks with recovery key hook
Update xcodeproj
Review fixups
Review fixups
Make the `SecureBackupRecoveryState.viewMode` extension private
Bump the enterprise submodule
Disabled in 5ba94609b because pause and resume could interleave and strand
the client suspended after a foreground resume. That race is now fixed, so
turn the feature back on for non-release builds.
The send queue auto-enable listener fires whenever the queue is believed
off and the homeserver is reachable. While the client is suspended this
could re-enable the queue and potentially fall straight back into a re-enable
loop.
Gate the listener on the desired service state so queues are only restarted
when we intend to be running, and nudge the listener on resume so the queue
recovers reliably. The SDK only re-enables queues when client.resume() runs,
which is gated behind clientPausingAndResumingEnabled, so the nudge keeps
recovery uniform across builds rather than depending on the flag.
`pauseServices` took a completion block only because it was historically
called from `deinit`, which can't await (the SE-0371 note). `deinit` now
stops the sync itself, so the sole remaining caller is the AppCoordinator,
which can await instead.
Make `pauseServices` async and update the AppCoordinator background-task
and lifecycle call sites to await it, calling the OS-facing completions
(endBackgroundTask / setTaskCompleted) afterwards. This also removes the
`MainActor.assumeIsolated` workaround — awaiting a @MainActor method hops
onto the main actor cleanly — and fixes a latent bug where a nil `self` in
the background app refresh expiration handler would skip setTaskCompleted.
Pause and resume previously ran as independent, unsynchronised tasks:
`pauseServices` stopped the sync and paused the client from a detached
`Task`, while `resumeServices` resumed and started the sync directly.
With no ordering between them, a late background pause could land after a
foreground resume — leaving the client suspended and the sync stopped
while the app was active, which broke everything until the next relaunch.
Route both through a single serialised reconciler: each request records
the desired state and chains onto the previous transition, and the
reconcile always drives the SDK to the *latest* desired state. Transitions
can no longer interleave and the most recent intent always wins.
On deinit we no longer pause the client — it's being deallocated, so the
SDK tears it down anyway; we just stop the sync and cancel the delegate.
Pasting a single link on top of a selection in the message composer now
produces a markdown link, e.g. selecting "Google" and pasting
"google.com" yields "[Google](google.com)". Non-link clipboard
contents and pastes without a selection keep their existing behaviour.
Mirror crashes into our own logs via Sentry's beforeSend hook so the full crash
log ships with rageshakes. Adds a SentryEvent extension to extract the formatted
crash log from the event.
iOS kills the app if a VoIP push isn't followed by reportNewIncomingCall, so
report an immediately ended call on every early-return path, named after the
room. A duplicate of an ongoing call is reported as answered elsewhere and an
expired push as a missed call that can be tapped to call back.
Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>