Commit Graph

311 Commits

Author SHA1 Message Date
Mauro 268d6c8ead Notify every signed-in account and turn notifications on and off per account (#6337)
* 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.
2026-10-15 13:10:26 +02:00
Doug b6ccbbc6c3 Add a new UserHooks container and move some AppHooks. (#6332)
* 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.
2026-10-15 10:49:27 +01:00
Mauro 7752ae3f6a Keep every signed-in account live (#6299)
* 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.
2026-10-13 17:06:05 +00:00
Stefan Ceriu 900b24bc5e Remove unnecessary iOS 26+ search screen restriction. 2026-10-13 19:00:43 +03:00
Doug ac29354b08 Move any visible account settings into AccountSettings. (#6308)
* 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.
2026-10-13 15:21:13 +01:00
Mauro Romito 5792a8ba5c Create sessions through the user session manager
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.
2026-10-09 15:21:27 +02:00
Mauro 60db364436 Introduce a user session manager (#6295) 2026-10-09 14:07:39 +02:00
Doug 65bab49f39 Remove the CommonSettingsProtocol and use UserSession to carry multiple dependencies. (#6302)
* Remove the CommonSettingsProtocol.

* Replace multiple dependencies with the UserSession when it carries them.
2026-10-09 10:20:00 +01:00
Stefan Ceriu 8b8384a108 Remove unnecessary type_body_length AppCoordinator SwiftLint rule 2026-10-09 11:44:39 +03:00
Doug fc75f90ef5 Remove the dynamic member lookup from UserSettings in favour of nested access. (#6294)
* Remove the dynamic member lookup from UserSettings in favour of nested access.

* Remove forwarding methods from UserSettings.

* Update the Enterprise submodule.
2026-10-08 15:19:44 +01:00
Mauro Romito fba28ed422 Fix the crash when signing back in after a soft logout
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.
2026-10-08 15:14:32 +02:00
Stefan Ceriu dafda65cbe Start the app coordinator from background tasks
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.
2026-10-07 18:21:55 +03:00
renovate[bot] 82a933d1d8 Update Project Dependencies (#6282)
* Update Project Dependencies

* Enable MetricKit for app hang diagnostics.

* Ignore the new first-party Call packages from our acknowledgements.
2026-10-07 10:15:50 +00:00
Doug 93825c01ad Initial implementation of AccountSettings. (#6275)
* 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..
2026-10-06 17:52:24 +01:00
Stefan Ceriu 52b411e954 Revert "Open the room live when tapping the notification for its newest message"
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.
2026-10-02 16:52:19 +03:00
Stefan Ceriu 11dc19f7ed Await the session restore instead of polling for it
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.
2026-10-01 15:59:42 +03:00
Stefan Ceriu a56a5c2819 Wait for the session restore in background app refresh
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
2026-10-01 15:59:42 +03:00
Mauro 831234865b Fix Swift 6.4 warnings (#6240) 2026-10-01 10:55:19 +02:00
Stefan Ceriu b21d27dbbc Jump straight to notification tap and external deep link targets
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.
2026-09-30 17:06:57 +03:00
Stefan Ceriu 4c71cae0f5 Open the room live when tapping the notification for its newest 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.
2026-09-30 14:50:04 +03:00
Stefan Ceriu 5a3c4ad2f5 Make the background app refresh task live in its own extension as well 2026-09-30 12:20:54 +03:00
Stefan Ceriu 49481f9af7 Add a search backfill app background processing task
To run when the device is idle, charging and connected to the network.
2026-09-30 12:20:54 +03:00
Doug fccad5fc30 Replace AppSettings with UserSettings from the UserSessionFlow and below.
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
2026-09-28 13:04:37 +03:00
Stefan Ceriu 44fb05f489 Make Sentry and PostHog default settings explicit
... and clearly disable the intrusive ones
2026-09-28 11:04:22 +03:00
Valere Fedronic fb49f5b83b Add experimental native calls behind a developer flag (#6159)
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.
2026-09-17 19:42:18 +03:00
Stefan Ceriu d4d74dd8f8 Avoid race conditions between the home screen's crash alert and Sentry's onLastRunStatusDetermined
By converting the `lastCrashEventID` to a publisher that the VM can just listen to instead of relying on start to match a pre-set event
2026-08-13 16:17:21 +03:00
Mauro Romito 748ec818ac remove map tiler url from matrix wellknown 2026-08-05 16:00:43 +02:00
Doug bf498a5d18 Small tweaks to the Presence feature. (#5918) 2026-07-23 08:40:48 +01:00
Jared L cff46137b9 Report user presence on app lifecycle transitions (#5838)
* 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>
2026-07-22 15:23:36 +01:00
Mauro 9093631f8b Periphery cleanup (#5893) 2026-07-20 11:16:11 +02:00
Matthew Hodgson 834938f585 don't keep re-prompting the user to migrate if they get spontaneously logged out. (#5844) 2026-07-07 15:28:40 +02:00
Doug ace05b97d0 Combine the user's own display name/avatar into a single profile.
This is in preparation for adding the user status fields into the profile.
2026-07-02 13:09:34 +01:00
Stefan Ceriu 2777075655 Handle the non-deprecated INStartCallIntent for calls from Recents
The call-from-Recents handling was wired entirely to the deprecated
INStartVideoCallIntent (the activity-type registration and the cast), which
also meant the app never handled the modern INStartCallIntent that superseded
it in iOS 13. Switch both to INStartCallIntent and derive isVoiceCall from its
callCapability. Clears the deprecation warning.
2026-06-29 18:10:30 +03:00
Doug f05fd41c36 Replace the UserPreference property wrapper with the new macro version. 2026-06-29 10:34:32 +01:00
Stefan Ceriu 10017714e5 Fix libdispatch main-thread trap when logging Sentry crash events
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.
2026-06-22 14:45:50 +03:00
Mauro a520d94760 Swift 6 Migration (#5733)
* 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>
2026-06-18 17:55:31 +02:00
Stefan Ceriu 02b114d40b Make ClientProxy.pauseServices async and drop its completion block
`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.
2026-06-17 15:17:06 +03:00
Stefan Ceriu 0ce7a5ea13 Log Sentry crash events through MXLog
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.
2026-06-16 12:13:10 +03:00
Stefan Ceriu 21d2f59b26 Rename AppCoordinator pause and resume methods to be more explicit on their target. 2026-06-05 14:54:36 +03:00
Stefan Ceriu 393abe6fa5 Make sure client services are resumed before handling app routes. 2026-06-05 14:54:36 +03:00
Stefan Ceriu 713af64fc6 Switch the clientProxy's start/stop sync semantics to service pausing and resuming to better match underlying behaviors 2026-06-05 14:54:36 +03:00
Stefan Ceriu e4a7322250 Expose the async nature of the ClientProxy startSync method 2026-06-05 14:54:36 +03:00
Mauro 7093d966b4 Set map tiler URL from well known if available (#5633) 2026-05-28 15:24:58 +02:00
Doug fa67713726 Run SwiftFormat. 2026-05-27 17:11:42 +01:00
Michael 22d14ff8e4 AppSettings are Mockable (#5589)
* 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>
2026-05-22 15:14:08 +02:00
Stefan Ceriu 97d4a35af7 Improve Application lifetime scheduling
We have noticed race conditions between deep link handling and client
resuming/sync starting which lead to timelines being created without being able
to access the underlying stores.

This patch switches to foreground/background app tracking in an attempt at a cheap
way to fix the underlying issue. Arguably this was the correct way to do it to
being with.
2026-05-22 15:41:17 +03:00
Michael 5a07c98afc Make AnalyticsService mockable via `AnalyticsServiceProtocol (#5599)
* 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
2026-05-20 08:23:58 +03:00
Stefan Ceriu 579b63d331 Remove unused method 2026-05-17 14:37:55 +03:00
Michael ff5dcb040d Replace the ServiceLocator with direct dependency injection (#5548)
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.
2026-05-14 09:45:49 +03:00
Doug 0355e9f708 Add an OAuthPresenterHook. (#5545)
* Add an OAuthPresenterHook.

* Update the enterprise submodule.
2026-05-06 10:29:25 +01:00