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>
This commit is contained in:
Mauro
2026-06-18 17:55:31 +02:00
committed by GitHub
parent 7edca8b5a1
commit a520d94760
444 changed files with 3677 additions and 3361 deletions
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol AppLockScreenViewModelProtocol {
var actions: AnyPublisher<AppLockScreenViewModelAction, Never> { get }
var context: AppLockScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol AppLockSetupBiometricsScreenViewModelProtocol {
var actions: AnyPublisher<AppLockSetupBiometricsScreenViewModelAction, Never> { get }
var context: AppLockSetupBiometricsScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol AppLockSetupPINScreenViewModelProtocol {
var actions: AnyPublisher<AppLockSetupPINScreenViewModelAction, Never> { get }
var context: AppLockSetupPINScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol AppLockSetupSettingsScreenViewModelProtocol {
var actions: AnyPublisher<AppLockSetupSettingsScreenViewModelAction, Never> { get }
var context: AppLockSetupSettingsScreenViewModelType.Context { get }
@@ -9,7 +9,7 @@
import Foundation
/// The supported forms of login that a homeserver allows.
enum LoginMode: Equatable {
nonisolated enum LoginMode: Equatable {
/// The login mode hasn't been determined yet.
case unknown
/// The homeserver supports login via OpenID Connect.
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol LoginScreenViewModelProtocol {
var actions: AnyPublisher<LoginScreenViewModelAction, Never> { get }
var context: LoginScreenViewModelType.Context { get }
@@ -13,7 +13,6 @@ import AuthenticationServices
/// In certain instances the URL may require opening an external app instead of using a WAS. Because of this
/// it is recommended to not encode the OAuth authentication within any state machines, as there is no guarantee
/// that any cancellations/failures will be communicated upwards.
@MainActor
class OAuthAuthenticationPresenter: NSObject {
private let authenticationService: AuthenticationServiceProtocol
private let redirectURL: URL
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol ServerConfirmationScreenViewModelProtocol {
var actions: AnyPublisher<ServerConfirmationScreenViewModelAction, Never> { get }
var context: ServerConfirmationScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol ServerSelectionScreenViewModelProtocol {
var actions: AnyPublisher<ServerSelectionScreenViewModelAction, Never> { get }
var context: ServerSelectionScreenViewModelType.Context { get }
@@ -22,7 +22,7 @@ enum MockSoftLogoutScreenState: String, CaseIterable {
case keyBackupNeeded
/// Generate the view struct for the screen state.
@MainActor var viewModel: SoftLogoutScreenViewModel {
var viewModel: SoftLogoutScreenViewModel {
let credentials = SoftLogoutScreenCredentials(userID: "@mock:matrix.org",
homeserverName: "matrix.org",
userDisplayName: "mock",
@@ -106,7 +106,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
private static let loadingIndicatorIdentifier = "\(SoftLogoutScreenCoordinator.self)-Loading"
/// Show an activity indicator whilst loading.
@MainActor private func startLoading() {
private func startLoading() {
parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
type: .modal,
title: L10n.commonLoading,
@@ -114,17 +114,17 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
}
/// Hide the currently displayed activity indicator.
@MainActor private func stopLoading() {
private func stopLoading() {
parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
}
/// Shows the forgot password screen.
@MainActor private func showForgotPasswordScreen() {
private func showForgotPasswordScreen() {
viewModel.displayError(.alert("Not implemented."))
}
/// Login with the supplied username and password.
@MainActor private func login(withPassword password: String) {
private func login(withPassword password: String) {
let username = parameters.credentials.userID
startLoading()
@@ -46,7 +46,7 @@ class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreen
}
}
@MainActor func displayError(_ type: SoftLogoutScreenErrorType) {
func displayError(_ type: SoftLogoutScreenErrorType) {
switch type {
case .alert(let message):
state.bindings.alertInfo = AlertInfo(id: type,
@@ -8,11 +8,10 @@
import Combine
@MainActor
protocol SoftLogoutScreenViewModelProtocol {
var actions: AnyPublisher<SoftLogoutScreenViewModelAction, Never> { get }
var context: SoftLogoutScreenViewModelType.Context { get }
/// Display an error to the user.
@MainActor func displayError(_ type: SoftLogoutScreenErrorType)
func displayError(_ type: SoftLogoutScreenErrorType)
}
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol AuthenticationStartScreenViewModelProtocol {
var actions: AnyPublisher<AuthenticationStartScreenViewModelAction, Never> { get }
var context: AuthenticationStartScreenViewModelType.Context { get }
@@ -69,20 +69,28 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers
state.blockedUsers = await withTaskGroup(of: UserProfileProxy.self) { group in
for userID in blockedUsers {
group.addTask {
switch await self.clientProxy.profile(for: userID) {
case .success(let profile): profile
case .failure: UserProfileProxy(userID: userID)
}
await self.profile(for: userID)
}
}
return await group.reduce(into: []) { partialResult, profile in
partialResult.append(profile)
var profiles = [UserProfileProxy]()
for await profile in group {
profiles.append(profile)
}
return profiles
}
}
}
/// The client proxy isn't Sendable, fetch through this helper so that it
/// never leaves the main actor when running calls in parallel.
private func profile(for userID: String) async -> UserProfileProxy {
switch await clientProxy.profile(for: userID) {
case .success(let profile): profile
case .failure: UserProfileProxy(userID: userID)
}
}
private func unblockUser(_ user: UserProfileProxy) {
showLoadingIndicator()
state.processingUserID = user.userID
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol BlockedUsersScreenViewModelProtocol {
var context: BlockedUsersScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol BugReportScreenViewModelProtocol {
var actions: AnyPublisher<BugReportScreenViewModelAction, Never> { get }
var context: BugReportScreenViewModelType.Context { get }
@@ -100,7 +100,10 @@ struct BugReportScreen: View {
PhotosPicker(selection: $selectedScreenshot,
matching: .screenshots,
photoLibrary: .shared()) {
ListRowLabel.plain(title: photosPickerTitle)
// The label builder isn't isolated in SwiftUI's signature but is only ever called on the main actor.
MainActor.assumeIsolated {
ListRowLabel.plain(title: photosPickerTitle)
}
}
})
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.attachScreenshot)
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol CallScreenViewModelProtocol {
var actions: AnyPublisher<CallScreenViewModelAction, Never> { get }
var context: CallScreenViewModelType.Context { get }
@@ -71,7 +71,6 @@ private struct CallView: UIViewRepresentable {
}
}
@MainActor
class Coordinator: NSObject, WKUIDelegate, WKNavigationDelegate, AVPictureInPictureControllerDelegate {
private weak var viewModelContext: CallScreenViewModel.Context?
@@ -168,6 +167,9 @@ private struct CallView: UIViewRepresentable {
if let error {
continuaton.resume(throwing: error)
} else {
// The completion is called on the main thread which the continuation
// also resumes on, so the result never actually crosses threads.
nonisolated(unsafe) let result = result
continuaton.resume(returning: result)
}
}
@@ -7,7 +7,6 @@
import Combine
@MainActor
protocol ChatsSpaceFiltersScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ChatsSpaceFiltersScreenViewModelAction, Never> { get }
var context: ChatsSpaceFiltersScreenViewModelType.Context { get }
@@ -49,7 +49,7 @@ struct ChatsSpaceFilterCell: View {
.padding(.horizontal, horizontalInsets)
}
@ViewBuilder @MainActor
@ViewBuilder
private var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: filter.room.avatar,
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol PollFormScreenViewModelProtocol {
var actions: AnyPublisher<PollFormScreenViewModelAction, Never> { get }
var context: PollFormScreenViewModelType.Context { get }
@@ -148,7 +148,7 @@ struct PollFormScreen: View {
}
private extension View {
@MainActor @ViewBuilder
@ViewBuilder
func trackAnalyticsIfNeeded(context: PollFormScreenViewModel.Context) -> some View {
switch context.viewState.mode {
case .edit:
@@ -303,9 +303,9 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen
private func addRoomToSpace(roomProxy: JoinedRoomProxyProtocol, spaceID: String) async {
roomProxy.subscribeToRoomInfoUpdates()
let runner = ExpiringTaskRunner {
let runner = ExpiringTaskRunner { @MainActor in
// Necessary to build the room cache so that the space can be added as a parent.
_ = await roomProxy.infoPublisher.values.first { $0.powerLevels != nil }
_ = await roomProxy.infoPublisher.values.first { @Sendable in $0.powerLevels != nil }
}
do {
@@ -9,7 +9,6 @@
import Combine
import Foundation
@MainActor
protocol CreateRoomScreenViewModelProtocol {
var actions: AnyPublisher<CreateRoomScreenViewModelAction, Never> { get }
var context: CreateRoomScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol DeclineAndBlockScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<DeclineAndBlockScreenViewModelAction, Never> { get }
var context: DeclineAndBlockScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol EditRoomAddressScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<EditRoomAddressScreenViewModelAction, Never> { get }
var context: EditRoomAddressScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol EmojiPickerScreenViewModelProtocol {
var actions: AnyPublisher<EmojiPickerScreenViewModelAction, Never> { get }
var context: EmojiPickerScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol EncryptionResetPasswordScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<EncryptionResetPasswordScreenViewModelAction, Never> { get }
var context: EncryptionResetPasswordScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol EncryptionResetScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<EncryptionResetScreenViewModelAction, Never> { get }
var context: EncryptionResetScreenViewModelType.Context { get }
@@ -153,7 +153,10 @@ class TimelineMediaPreviewController: QLPreviewController {
// Ridiculous hack to undo the controller's attempt to replace our info button with the list button.
if barButtonTimer == nil {
barButtonTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.updateBarButtons()
// The timer is scheduled on the main run loop so it always fires on the main actor.
MainActor.assumeIsolated {
self?.updateBarButtons()
}
}
}
}
@@ -20,7 +20,6 @@ enum GlobalSearchControllerAction {
case select(roomID: String)
}
@MainActor
class GlobalSearchScreenCoordinator: CoordinatorProtocol {
private let viewModel: GlobalSearchScreenViewModelProtocol
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol GlobalSearchScreenViewModelProtocol {
var actions: AnyPublisher<GlobalSearchScreenViewModelAction, Never> { get }
var context: GlobalSearchScreenViewModelType.Context { get }
@@ -24,7 +24,7 @@ struct GlobalSearchScreenListRow: View {
}
}
@ViewBuilder @MainActor
@ViewBuilder
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol HomeScreenViewModelProtocol {
var actions: AnyPublisher<HomeScreenViewModelAction, Never> { get }
@@ -10,7 +10,6 @@ import Combine
import Compound
import SwiftUI
@MainActor
struct HomeScreenInviteCell: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@@ -209,7 +208,6 @@ struct HomeScreenInviteCell_Previews: PreviewProvider, TestablePreview {
}
}
@MainActor
private extension HomeScreenRoom {
static var dmInvite: HomeScreenRoom {
let inviter = RoomMemberProxyMock()
@@ -10,7 +10,6 @@ import Combine
import Compound
import SwiftUI
@MainActor
struct HomeScreenKnockedCell: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
@@ -141,7 +140,6 @@ struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview {
}
}
@MainActor
private extension HomeScreenRoom {
static var dmInvite: HomeScreenRoom {
let inviter = RoomMemberProxyMock()
@@ -44,7 +44,7 @@ struct HomeScreenRoomCell: View {
.accessibilityHidden(redactionReasons.contains(.placeholder) ? true : false)
}
@ViewBuilder @MainActor
@ViewBuilder
private var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
@@ -211,10 +211,10 @@ import MatrixRustSDKMocks
struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview {
static let summaryProviderGeneric = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
static let genericRooms = summaryProviderGeneric.roomListPublisher.value.compactMap(mockRoom)
static let genericRooms = summaryProviderGeneric.roomListPublisher.value.compactMap { mockRoom(summary: $0) }
static let summaryProviderForNotificationsState = RoomSummaryProviderMock(.init(state: .loaded(.mockRoomsWithNotificationsState)))
static let notificationsStateRooms = summaryProviderForNotificationsState.roomListPublisher.value.compactMap(mockRoom)
static let notificationsStateRooms = summaryProviderForNotificationsState.roomListPublisher.value.compactMap { mockRoom(summary: $0) }
static let lastMessageStateRooms = [makeRoom(lastMessageState: .sending), makeRoom(lastMessageState: .failed)]
@@ -156,9 +156,11 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
}
}
return await group.first { inviteResult in
inviteResult.isFailure
} ?? .success(())
for await inviteResult in group where inviteResult.isFailure {
return inviteResult
}
return .success(())
}
guard case .failure = result else {
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol InviteUsersScreenViewModelProtocol {
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> { get }
var context: InviteUsersScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol JoinRoomScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<JoinRoomScreenViewModelAction, Never> { get }
var context: JoinRoomScreenViewModelType.Context { get }
@@ -386,7 +386,6 @@ struct JoinRoomScreenSpace_Previews: PreviewProvider, TestablePreview {
}
}
@MainActor
struct JoinRoomScreenPreviewWrapper: Identifiable {
let id = UUID()
let viewModel: JoinRoomScreenViewModel
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol KnockRequestsListScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<KnockRequestsListScreenViewModelAction, Never> { get }
var context: KnockRequestsListScreenViewModelType.Context { get }
@@ -29,9 +29,9 @@ struct KnockRequestCellInfo: Equatable {
struct KnockRequestCell: View {
let cellInfo: KnockRequestCellInfo
var mediaProvider: MediaProviderProtocol?
let onAccept: ((String) -> Void)?
let onDecline: ((String) -> Void)?
let onDeclineAndBan: ((String) -> Void)?
let onAccept: (@MainActor (String) -> Void)?
let onDecline: (@MainActor (String) -> Void)?
let onDeclineAndBan: (@MainActor (String) -> Void)?
var body: some View {
HStack(alignment: .top, spacing: 16) {
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol LabsScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<LabsScreenViewModelAction, Never> { get }
var context: LabsScreenViewModelType.Context { get }
@@ -14,7 +14,7 @@ enum LinkNewDeviceScreenViewModelAction {
}
struct LinkNewDeviceScreenViewState: BindableState {
enum Mode: Equatable {
nonisolated enum Mode: Equatable {
case loading
case readyToLink(isGeneratingCode: Bool)
case error(QRCodeLoginState.ErrorState)
@@ -69,13 +69,12 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev
let progressPublisher = linkNewDeviceService.linkMobileDevice()
do {
_ = try await progressPublisher.values
.first { progress in
switch progress {
case .qrReady: true
default: false
}
var iterator = progressPublisher.values.makeAsyncIterator()
while let progress = try await iterator.next(isolation: #isolation) {
if case .qrReady = progress {
break
}
}
actionsSubject.send(.linkMobileDevice(progressPublisher))
state.mode = .readyToLink(isGeneratingCode: false)
@@ -7,7 +7,6 @@
import Combine
@MainActor
protocol LinkNewDeviceScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<LinkNewDeviceScreenViewModelAction, Never> { get }
var context: LinkNewDeviceScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol LocationSharingScreenViewModelProtocol {
var actions: AnyPublisher<LocationSharingScreenViewModelAction, Never> { get }
var context: LocationSharingScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol LogViewerScreenViewModelProtocol {
var actions: AnyPublisher<LogViewerScreenViewModelAction, Never> { get }
var context: LogViewerScreenViewModelType.Context { get }
@@ -7,7 +7,6 @@
import Combine
@MainActor
protocol ManageAuthorizedSpacesScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ManageAuthorizedSpacesScreenViewModelAction, Never> { get }
var context: ManageAuthorizedSpacesScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol ManageRoomMemberSheetViewModelProtocol {
var actions: AnyPublisher<ManageRoomMemberSheetViewModelAction, Never> { get }
var context: ManageRoomMemberSheetViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol MediaEventsTimelineScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<MediaEventsTimelineScreenViewModelAction, Never> { get }
var context: MediaEventsTimelineScreenViewModelType.Context { get }
@@ -125,7 +125,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType,
}
private func sendAttachment(mediaInfo: MediaInfo, caption: String?) async -> Result<Void, TimelineControllerError> {
let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in
let requestHandle: (@MainActor @Sendable (SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in
self?.requestHandle = handle
}
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol MediaUploadPreviewScreenViewModelProtocol {
var actions: AnyPublisher<MediaUploadPreviewScreenViewModelAction, Never> { get }
var context: MediaUploadPreviewScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol MessageForwardingScreenViewModelProtocol {
var actions: AnyPublisher<MessageForwardingScreenViewModelAction, Never> { get }
var context: MessageForwardingScreenViewModelType.Context { get }
@@ -79,7 +79,7 @@ private struct MessageForwardingListRow: View {
})
}
@ViewBuilder @MainActor
@ViewBuilder
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol AnalyticsPromptScreenViewModelProtocol {
var actions: AnyPublisher<AnalyticsPromptScreenViewModelAction, Never> { get }
var context: AnalyticsPromptScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol IdentityConfirmationScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<IdentityConfirmationScreenViewModelAction, Never> { get }
var context: IdentityConfirmationScreenViewModelType.Context { get }
@@ -7,7 +7,6 @@
import Combine
@MainActor
protocol IdentityConfirmedScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<IdentityConfirmedScreenViewModelAction, Never> { get }
var context: IdentityConfirmedScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol NotificationPermissionsScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<NotificationPermissionsScreenViewModelAction, Never> { get }
var context: NotificationPermissionsScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol SessionVerificationScreenViewModelProtocol {
var actions: AnyPublisher<SessionVerificationScreenViewModelAction, Never> { get }
var context: SessionVerificationViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol PinnedEventsTimelineScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<PinnedEventsTimelineScreenViewModelAction, Never> { get }
var context: PinnedEventsTimelineScreenViewModelType.Context { get }
@@ -95,7 +95,7 @@ enum QRCodeLoginScreenViewAction {
case errorAction(QRCodeErrorView.Action)
}
enum QRCodeLoginState: Equatable {
nonisolated enum QRCodeLoginState: Equatable {
/// Initial state where the user is informed how to login this device by scanning a QR code.
case loginInstructions
/// Initial state where the user is informed how to link another device by scanning it's QR code.
@@ -114,7 +114,7 @@ enum QRCodeLoginState: Equatable {
/// Any full screen error state
case error(ErrorState)
enum ErrorState: Equatable, CaseIterable {
nonisolated enum ErrorState: Equatable, CaseIterable {
/// The account provider doesn't support the use of QR codes.
case notSupported
case noCameraPermission
@@ -131,7 +131,7 @@ enum QRCodeLoginState: Equatable {
case unknown
}
enum ScanningState: Equatable {
nonisolated enum ScanningState: Equatable {
/// The QR code is scanning.
case scanning
/// The QR code has been detected and is being processed.
@@ -139,7 +139,7 @@ enum QRCodeLoginState: Equatable {
/// The QR code was scanned, but an error occurred.
case scanFailed(Error)
enum Error: Equatable {
nonisolated enum Error: Equatable {
/// The QR code has been processed and is invalid.
case invalid
/// The QR code has been processed but it is for an account provider that isn't allowed.
@@ -171,14 +171,14 @@ enum QRCodeLoginState: Equatable {
}
}
enum DisplayQRState: Equatable {
nonisolated enum DisplayQRState: Equatable {
/// The QR code is available and is being shown.
case active(UIImage)
/// The QR code being shown has expired. We will need to generate a new one.
case expired
}
enum DisplayCodeState: Equatable {
nonisolated enum DisplayCodeState: Equatable {
case deviceCode(String)
case verificationCode(String)
@@ -190,7 +190,7 @@ enum QRCodeLoginState: Equatable {
}
}
enum CheckCodeState: Equatable {
nonisolated enum CheckCodeState: Equatable {
/// The user needs to input the confirmation code.
case inputCode(CheckCodeSenderProxy)
/// The code supplied by the user didn't pass local validation.
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol QRCodeLoginScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<QRCodeLoginScreenViewModelAction, Never> { get }
var context: QRCodeLoginScreenViewModelType.Context { get }
@@ -109,17 +109,19 @@ final class QRScannerController: UIViewController {
}
func startScan() {
// Start video capture.
// AVCaptureSession start/stop are blocking calls that Apple documents should be
// made off the main thread, the session synchronises access internally.
nonisolated(unsafe) let captureSession = captureSession
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession.startRunning()
captureSession.startRunning()
MXLog.info("QRCodeScannerView: capture session started")
}
}
func stopScan() {
// Stop video capture.
nonisolated(unsafe) let captureSession = captureSession
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession.stopRunning()
captureSession.stopRunning()
MXLog.info("QRCodeScannerView: capture session stopped")
}
}
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol ReportContentScreenViewModelProtocol {
var actions: AnyPublisher<ReportContentScreenViewModelAction, Never> { get }
var context: ReportContentScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol ReportRoomScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ReportRoomScreenViewModelAction, Never> { get }
var context: ReportRoomScreenViewModelType.Context { get }
@@ -111,7 +111,6 @@ class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFail
// MARK: - Iterators
@MainActor
private protocol VerifiedUserSendFailureIterator {
func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)?
}
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol ResolveVerifiedUserSendFailureScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<ResolveVerifiedUserSendFailureScreenViewModelAction, Never> { get }
var context: ResolveVerifiedUserSendFailureScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomChangePermissionsScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomChangePermissionsScreenViewModelAction, Never> { get }
var context: RoomChangePermissionsScreenViewModelType.Context { get }
@@ -165,7 +165,11 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
// A task we can await until the room's info gets modified with the new power levels.
// Note: Ignore the first value as the publisher is backed by a current value subject.
let infoTask = Task { await roomProxy.infoPublisher.dropFirst().values.first { _ in true } }
let infoTask = Task {
var iterator = roomProxy.infoPublisher.values.makeAsyncIterator()
_ = await iterator.next(isolation: #isolation) // The publisher's current value.
_ = await iterator.next(isolation: #isolation)
}
switch await roomProxy.updatePowerLevelsForUsers(promotingUpdates + demotingUpdates) {
case .success:
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomChangeRolesScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomChangeRolesScreenViewModelAction, Never> { get }
var context: RoomChangeRolesScreenViewModelType.Context { get }
@@ -138,23 +138,19 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
try await withThrowingTaskGroup(of: Void.self) { group in
if state.avatarDidChange {
group.addTask {
if let localMedia = await self.state.localMedia {
try await self.roomProxy.uploadAvatar(media: localMedia).get()
} else if await self.state.avatarURL == nil {
try await self.roomProxy.removeAvatar().get()
}
try await self.saveAvatar()
}
}
if state.nameDidChange {
group.addTask {
try await self.roomProxy.setName(self.state.bindings.name).get()
try await self.saveName()
}
}
if state.topicDidChange {
group.addTask {
try await self.roomProxy.setTopic(self.state.bindings.topic).get()
try await self.saveTopic()
}
}
@@ -168,4 +164,20 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
message: L10n.screenRoomDetailsEditionError)
}
}
private func saveAvatar() async throws {
if let localMedia = state.localMedia {
try await roomProxy.uploadAvatar(media: localMedia).get()
} else if state.avatarURL == nil {
try await roomProxy.removeAvatar().get()
}
}
private func saveName() async throws {
try await roomProxy.setName(state.bindings.name).get()
}
private func saveTopic() async throws {
try await roomProxy.setTopic(state.bindings.topic).get()
}
}
@@ -9,7 +9,6 @@
import Combine
import Foundation
@MainActor
protocol RoomDetailsEditScreenViewModelProtocol {
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> { get }
var context: RoomDetailsEditScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomDetailsScreenViewModelProtocol {
var actions: AnyPublisher<RoomDetailsScreenViewModelAction, Never> { get }
var context: RoomDetailsScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomDirectorySearchScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomDirectorySearchScreenViewModelAction, Never> { get }
var context: RoomDirectorySearchScreenViewModelType.Context { get }
@@ -142,7 +142,6 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
}
}
@MainActor
private func unignoreUser() async {
guard let roomMemberProxy else {
fatalError()
@@ -164,7 +163,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro
}
private func updateMembers() {
Task.detached {
Task {
await self.roomProxy.updateMembers()
}
}
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomMemberDetailsScreenViewModelProtocol {
var actions: AnyPublisher<RoomMemberDetailsScreenViewModelAction, Never> { get }
var context: RoomMemberDetailsScreenViewModelType.Context { get }
@@ -30,7 +30,7 @@ enum RoomMembersListScreenMode {
case banned
}
struct RoomMemberListScreenEntry: Equatable {
nonisolated struct RoomMemberListScreenEntry: Equatable {
let member: RoomMemberDetails
let verificationState: UserIdentityVerificationState
}
@@ -123,7 +123,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
}
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
await Task.detached { [userSession, roomProxy] in
// We don't care about identity statuses on non-encrypted rooms
let isEncrypted = roomProxy.infoPublisher.value.isEncrypted
return await Task.detached { [weak self] in
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
var invitedMembers: [RoomMemberListScreenEntry] = .init()
var joinedMembers: [RoomMemberListScreenEntry] = .init()
@@ -131,10 +134,8 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
for member in members {
var verificationState: UserIdentityVerificationState = .notVerified
if roomProxy.infoPublisher.value.isEncrypted, // We don't care about identity statuses on non-encrypted rooms
case let .success(userIdentity) = await userSession.clientProxy.userIdentity(for: member.userID, fallBackToServer: false),
let userIdentity {
verificationState = userIdentity.verificationState
if isEncrypted, let fetchedState = await self?.userIdentityVerificationState(for: member.userID) {
verificationState = fetchedState
}
switch member.membership {
@@ -156,6 +157,15 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
.value
}
/// The client proxy isn't Sendable, fetch identities through this helper so
/// that it never leaves the main actor.
private func userIdentityVerificationState(for userID: String) async -> UserIdentityVerificationState? {
guard case let .success(userIdentity) = await userSession.clientProxy.userIdentity(for: userID, fallBackToServer: false) else {
return nil
}
return userIdentity?.verificationState
}
private func selectMember(_ member: RoomMemberDetails) {
guard currentUserProxy?.userID != member.id else {
showMemberDetails(member)
@@ -226,7 +236,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
}
}
private struct RoomMembersDetails {
private nonisolated struct RoomMembersDetails {
var invitedMembers: [RoomMemberListScreenEntry]
var joinedMembers: [RoomMemberListScreenEntry]
var bannedMembers: [RoomMemberListScreenEntry]
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomMembersListScreenViewModelProtocol {
var actions: AnyPublisher<RoomMembersListScreenViewModelAction, Never> { get }
var context: RoomMembersListScreenViewModelType.Context { get }
@@ -128,7 +128,6 @@ struct RoomMembersListScreen: View {
}
}
@MainActor
private enum MembersSection {
case joined
case invited
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomNotificationSettingsScreenViewModelProtocol {
var actions: AnyPublisher<RoomNotificationSettingsScreenViewModelAction, Never> { get }
var context: RoomNotificationSettingsScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomPollsHistoryScreenViewModelProtocol {
var actions: AnyPublisher<RoomPollsHistoryScreenViewModelAction, Never> { get }
var context: RoomPollsHistoryScreenViewModelType.Context { get }
@@ -100,7 +100,11 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
// A task we can await until the room's info gets modified with the new power levels.
// Note: Ignore the first value as the publisher is backed by a current value subject.
let infoTask = Task { await roomProxy.infoPublisher.dropFirst().values.first { _ in true } }
let infoTask = Task {
var iterator = roomProxy.infoPublisher.values.makeAsyncIterator()
_ = await iterator.next(isolation: #isolation) // The publisher's current value.
_ = await iterator.next(isolation: #isolation)
}
switch await roomProxy.updatePowerLevelsForUsers([(userID: roomProxy.ownUserID, powerLevel: role.powerLevelValue)]) {
case .success:
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomRolesAndPermissionsScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomRolesAndPermissionsScreenViewModelAction, Never> { get }
var context: RoomRolesAndPermissionsScreenViewModelType.Context { get }
@@ -405,7 +405,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
repeat { // Don't enumerate and mutate at the same time, big no no
shouldMakeAnotherPass = false
attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, stop in
attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { @Sendable value, range, stop in
guard let value else { return }
shouldMakeAnotherPass = true
@@ -429,7 +429,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool
repeat {
shouldMakeAnotherPass = false
attributedString.enumerateAttribute(.MatrixAllUsersMention, in: .init(location: 0, length: attributedString.length), options: []) { value, range, stop in
attributedString.enumerateAttribute(.MatrixAllUsersMention, in: .init(location: 0, length: attributedString.length), options: []) { @Sendable value, range, stop in
guard value != nil else { return }
shouldMakeAnotherPass = true
@@ -10,7 +10,6 @@ import Combine
import WysiwygComposer
// periphery: ignore - markdown protocol
@MainActor
protocol ComposerToolbarViewModelProtocol {
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> { get }
var context: ComposerToolbarViewModelType.Context { get }
@@ -38,7 +38,6 @@ struct FormattingToolbar: View {
}
}
@MainActor
private extension FormatItem {
var foregroundColor: Color {
switch state {
@@ -120,7 +120,6 @@ enum RoomScreenFooterViewDetails {
case verificationViolation(member: RoomMemberProxyProtocol, learnMoreURL: URL)
}
@MainActor
enum PinnedEventsBannerState: Equatable {
case loading(numbersOfEvents: Int)
case loaded(state: PinnedEventsState)
@@ -225,7 +224,6 @@ enum PinnedEventsBannerState: Equatable {
}
}
@MainActor
struct PinnedEventsState: Equatable {
var pinnedEventContents: OrderedDictionary<String, AttributedString> = [:] {
didSet {
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomScreenViewModelProtocol {
var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get }
var context: RoomScreenViewModel.Context { get }
@@ -19,9 +19,9 @@ struct KnockRequestInfo: Equatable {
struct KnockRequestsBannerView: View {
let requests: [KnockRequestInfo]
let onDismiss: () -> Void
let onAccept: ((String) -> Void)?
let onViewAll: () -> Void
let onDismiss: @MainActor () -> Void
let onAccept: (@MainActor (String) -> Void)?
let onViewAll: @MainActor () -> Void
var mediaProvider: MediaProviderProtocol?
var body: some View {
@@ -50,9 +50,9 @@ struct KnockRequestsBannerView: View {
private struct SingleKnockRequestBannerContent: View {
let request: KnockRequestInfo
let onDismiss: () -> Void
let onAccept: ((String) -> Void)?
let onViewAll: () -> Void
let onDismiss: @MainActor () -> Void
let onAccept: (@MainActor (String) -> Void)?
let onViewAll: @MainActor () -> Void
var mediaProvider: MediaProviderProtocol?
var body: some View {
@@ -118,8 +118,8 @@ private struct SingleKnockRequestBannerContent: View {
private struct MultipleKnockRequestsBannerContent: View {
let requests: [KnockRequestInfo]
let onDismiss: () -> Void
let onViewAll: () -> Void
let onDismiss: @MainActor () -> Void
let onViewAll: @MainActor () -> Void
var mediaProvider: MediaProviderProtocol?
private var avatars: [StackedAvatarInfo] {
@@ -162,7 +162,7 @@ private struct MultipleKnockRequestsBannerContent: View {
}
private struct KnockRequestsBannerDismissButton: View {
let onDismiss: () -> Void
let onDismiss: @MainActor () -> Void
var body: some View {
Button {
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol RoomSelectionScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomSelectionScreenViewModelAction, Never> { get }
var context: RoomSelectionScreenViewModelType.Context { get }
@@ -78,7 +78,7 @@ private struct RoomSelectionListRow: View {
})
}
@ViewBuilder @MainActor
@ViewBuilder
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
RoomAvatarImage(avatar: room.avatar,
@@ -7,7 +7,6 @@
import Combine
@MainActor
protocol RoomThreadListScreenViewModelProtocol {
var actionsPublisher: AnyPublisher<RoomThreadListScreenViewModelAction, Never> { get }
var context: RoomThreadListScreenViewModelType.Context { get }
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol SecureBackupKeyBackupScreenViewModelProtocol {
var actions: AnyPublisher<SecureBackupKeyBackupScreenViewModelAction, Never> { get }
var context: SecureBackupKeyBackupScreenViewModelType.Context { get }
@@ -14,7 +14,7 @@ enum SecureBackupLogoutConfirmationScreenViewModelAction {
case logout
}
enum SecureBackupLogoutConfirmationScreenViewMode: Equatable {
nonisolated enum SecureBackupLogoutConfirmationScreenViewMode: Equatable {
case saveRecoveryKey
case waitingToStart(hasStalled: Bool)
case backupOngoing(progress: Double)
@@ -8,7 +8,6 @@
import Combine
@MainActor
protocol SecureBackupLogoutConfirmationScreenViewModelProtocol {
var actions: AnyPublisher<SecureBackupLogoutConfirmationScreenViewModelAction, Never> { get }
var context: SecureBackupLogoutConfirmationScreenViewModelType.Context { get }

Some files were not shown because too many files have changed in this diff Show More