* 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.
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.
* 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>
* 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>
* Use our standard XYZMock(.init()) pattern everywhere instead of XYZMock.default.
* Remove the configuration parameter label on mock convenience inits.
* Update the enterprise submodule.
* create DependenciesProtocol and replace ServiceLocator registration with it
replace mocking calls for ServiceLocator to Dependencies
replace ServiceLocator with instance level calls
replace ServiceLocator configs with TestDependencies config in Tests
replace servicelocator usage in analytics service
replace remaining ServiceLocator calls in unit tests
that depended on the UnitTestAppCoordinator configuration with per suite dependencies instances
fix swiftlint warning
remove remaining references to ServiceLocator
make dependencies properties consistent in tests
delete Dependencies and DependenciesProtocol
fix whitespace modifications
updates from PR feedback
a pass over converting AppSettings and AnalyticsService test instance properties to per test instantiation
fix a missed PR feedback
Sync up after rebase
create DependenciesProtocol and replace ServiceLocator registration with it
replace mocking calls for ServiceLocator to Dependencies
replace ServiceLocator with instance level calls
replace ServiceLocator configs with TestDependencies config in Tests
replace servicelocator usage in analytics service
replace remaining ServiceLocator calls in unit tests
that depended on the UnitTestAppCoordinator configuration with per suite dependencies instances
fix swiftlint warning
remove remaining references to ServiceLocator
make dependencies properties consistent in tests
delete Dependencies and DependenciesProtocol
* AppSettings is instance level mockable now
* replace mocked and preview usages of reset + AppSettings global init with
AppSettings(store: UserDefaultsMock())
* AppSettings.UserDefaultsKeys is now a RawRep struct, making it extensible
* update tests with new AppSettings so it builds
* update remaining AppSettings test/mock inits
* AppSettings.UserDefaultsKey conformed to ExpressibleByStringLiteral
* addressed issues found in first pass ai review
* update AppSettings instantiations after rebase
* fix accessibility tests coordinator after rebase
* Rename to VolatileUserDefaults
* AppSettings init uses mandatory parameter
* Revert UserDefaultsKey to enum
* Add TrackedUserDefaults to be able to reset the defaults object
* Added volatile static func to AppSettings
* Use .volatile() static func instead of full init
* review fixups
* Resolve build issues after rebase
* updated enterprise module
* remove the static reset settings call from the preview tests
---------
Co-authored-by: Mauro Romito <mauro.romito@element.io>
* Timeline: add a Jump to Unread button and a new-message badge on the scroll-to-bottom button.
* Rename scrollToFirstUnread to scrollToReadMarker for consistency with isReadMarkerVisible.
* Cache readMarkerUniqueID and unreadMessageCount as stored properties on TimelineState.
* TimelineViewModel: rename newTimelineItems and drop redundant ordering comment.
* Use Compound chevron icons in TimelineScrollButton.
* Fix TimelineScrollButton layout at large dynamic type sizes.
* Add a trailingAccessory slot to topBanners and use it to dodge banners with the jump-to-read-marker button.
* Put jump-to-read-marker behind a developer-options feature flag.
* Fine-tune TimelineScrollButton sizing and badge colour.
* Remove TimelineScrollButton chevron offset that was compensating for SF Symbol centering.
* Drop the new-messages count, replace with a presence dot.
* Move both timeline scroll buttons to bottom-right and revert topBanners trailingAccessory slot.
* Position TimelineScrollButton badge above for up direction and below for down direction.
* Add long-press mark-as-read pill to the timeline scroll buttons
* Read isSwitchingTimelines from state in updateHasNewMessagesAtBottom.
* Use assertionFailure when scrollToReadMarker is called with no read marker.
* Add TimelineScrollButton preview snapshots.
* (fix) design and various self-review issues
* (fix) suppress scroll button tap after long-press
* (feat) fall back to RoomInfo.fullyReadEventID for jump-to-unread
* (fix) hide jump-to-unread immediately after mark-as-read
* (fix) align with newer matrix-rust-sdk surface
* Bump the RustSDK to v26.05.20
---------
Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>
Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
* Create AnalyticsServiceProtocol and conform AnalyticsService
* Replace AnalyticsService properties and arguments with AnalyticsServiceProtocol
* Inline AnalyticsServiceMock replacing old mock
* handle more complex AnalyticsServiceMock callsites
* move documentation from AnalyticsService to AnalyticsServiceProtocol
Removes the ServiceLocator singleton and instead directly injects the dependencies it was holding for more control over instantiation of the various component subtrees and better visibility over how they're being used.
* Implemented opening the LLS on our own user when tapping on the banner
* added a test for moving the map as soon as the first update arrives.
* updated preview tests
* Live Location Sharing Banner
# Conflicts:
# ElementX.xcodeproj/project.pbxproj
* updated the top banner modifier and the top banner views
# Conflicts:
# ElementX/Resources/Localizations/en-US.lproj/Localizable.strings
# ElementX/Resources/Localizations/en.lproj/Localizable.strings
* improved the stopping function from the timeline item
* stop live location sharing before starting a new one.
* added some tests for LiveLocationManager
* pr suggestions
* feat: Start voice call from DM
* rename voiceCall:bool to isVoiceCall
* review: Fix a typo
* review: use one displayCall(bool) instead of 2 actions
* review: Add a new specific preview for DM calls
* combine startCall and startVoiceCall in single enum with isVoiceCall
* review: add isVoiceCall to presentCallScreen action
* review: Use proper a11y for voice vs video
* add voice/video options to UserProfile Screen
* fixup: move config params to the roomInfo object
* review: Revert changes on preview as the toolbar cannot be snapshot'd
* review: Extract call controls in specific file
* oups: Add voice call option in room details screen
* Update room details screenshots
* Update user profile screenshots
* Update room member details screenshots
* fixup: remove dead code
* Update Send button bg color
Change the gradient bg to accent/rest.
* Tidy-up ComposerToolbar to match iOS 18 Figma.
Also simplifies the tests a bit.
* Add a .glassEffect to Compound's SendButton.
* Add a border to TimelineReplyView.
Also use the same sizes in both the message bubbles and the composer.
* Change icon size and container in message bubbles
- Container size = 36x36px
- Icon size = 24x24px
* Update icon of reply contents to be 24x24
* Update the VoiceMessageButton to match the designs.
* Adopt Liquid Glass in the ComposerToolbar.
* Generate and fix snapshots.
---------
Co-authored-by: Doug <douglase@element.io>
* Update dependency element-hq/compound-design-tokens to v6.10.0
* Further Compound updates.
The gradients have been simplified and are now opaque.
* Missed snapshots.
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Doug <douglase@element.io>
* replace NavigationStack with ElementNavigationStack to allow the content to be rendered without a NavigationStack in a11y tests
* fix a11y tests
* update xcodeproject
* swiftformat fix
* use iOS 26.1 for CI
* use a wrapper to solve the issue for a11y tests
* ElementNavigationStack only uses the trick in DEBUG mode, and added a swiftlint rule to prevent the usage of NavigationStack
* feat: Display an icon in the room header for rooms with shared history
* fix: Apply suggestions from code review
- Simplifies `isRoomHistoryShared` expressions using type inferrence.
- Adds failure messages to unit test.
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
* fix: Remove extension method in favour of field on configuration.
* fix: Distinguish between `shared` and `worldReadable` icons.
* refactor: Use `RoomHistorySharingState` enum with extension methods
---------
Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
* Revert "Do not show history visible banner when the user cannot send messages. (#4892)"
This reverts commit 2e09914cb7.
* Revert "Add a banner to encrypted rooms with visible history. (#4851)"
This reverts commit 6153480d90.
* Expand the space flow UI tests to include the add rooms screen.
Also fixes some failures in these tests due to the room/space title now being a button.
* Expand the space flow UI tests to include the space members screen.
* Reset AppSettings before running preview tests.
* Add a banner to encrypted rooms with visible history. (#4738)
* feat: Add history visible alert.
- Adds a dismissable alert that is displayed whenever the user
opens a room with `history_visibility` != `joined`. When cleared,
this is recorded in the app's data store.
- When opening a room with `history_visibility` = `joined`, this
flag is cleared.
Issue: element-hq/element-meta#2875
* tests: Add unit tests for history sharing in `RoomScreenFooterView`.
* feat: Rename flag to `hasSeenHistoryVisibleBannerRooms`, document.
* refactor: Merge enum variants, use function for banner description.
* feat: Use `AppSettings.historyVisibleDetailsURL` over hard-coded value.
* tests: Correct potential race condition with deferred assertion.
* chore: Use Localazy translation string over project-defined.
* fix: Final tweaks and review comments.
* chore: Checkout `Enterprise` submodule.
* tests: Final fixes.
* fix: Condition banner visibility on feature flag state.
* fix: Prioritise identity violations over history visibility banner.
* tests: Add snapshots for history visible banner.
* tests: Use deferred failure timeout for improved test power.
* chore: Tweaks to spelling, simplify state logic.
* fix: Remove "g".
* fix: Show banner for shared/world-readable rooms, not invited.
* refactor: Use `else-if` instead of `if`.
* feat: Add history visible alert.
- Adds a dismissable alert that is displayed whenever the user
opens a room with `history_visibility` != `joined`. When cleared,
this is recorded in the app's data store.
- When opening a room with `history_visibility` = `joined`, this
flag is cleared.
Issue: element-hq/element-meta#2875
* tests: Add unit tests for history sharing in `RoomScreenFooterView`.
* feat: Rename flag to `hasSeenHistoryVisibleBannerRooms`, document.
* refactor: Merge enum variants, use function for banner description.
* feat: Use `AppSettings.historyVisibleDetailsURL` over hard-coded value.
* tests: Correct potential race condition with deferred assertion.
* chore: Use Localazy translation string over project-defined.
* fix: Final tweaks and review comments.
* chore: Checkout `Enterprise` submodule.
* tests: Final fixes.