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