From 922ebf47e6c07f87260c94c08377cdc51ea0f353 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 6 Feb 2025 11:35:23 +0200 Subject: [PATCH] Fix some concurrency warnings, update missed licence headers. (#3741) * Switch the TimelineController to an async sequence and fix the warnings on the UserIndicatorController --- .githooks/pre-push | 2 +- .../Application/AppCoordinatorProtocol.swift | 1 + .../Other/Extensions/Snapshotting.swift | 15 +- .../SwiftUI/ViewModel/BindableState.swift | 1 + .../UserIndicatorController.swift | 25 ++-- .../SoftLogoutScreenViewModelProtocol.swift | 1 + .../View/WebRegistrationScreen.swift | 10 +- .../Screens/CallScreen/View/CallScreen.swift | 16 +-- ...ReportContentScreenViewModelProtocol.swift | 1 - ...fiedUserSendFailureScreenCoordinator.swift | 15 +- ...eVerifiedUserSendFailureScreenModels.swift | 15 +- ...rifiedUserSendFailureScreenViewModel.swift | 15 +- ...erSendFailureScreenViewModelProtocol.swift | 15 +- ...ResolveVerifiedUserSendFailureScreen.swift | 15 +- .../ComposerToolbarViewModelProtocol.swift | 1 + .../RoomScreenViewModelProtocol.swift | 2 +- .../Sources/Services/Client/ClientProxy.swift | 14 +- .../ComposerDraft/ComposerDraftService.swift | 2 +- .../TimelineController.swift | 130 ++++++++++-------- .../RoomTimelineItemFactoryProtocol.swift | 1 - ...dUserSendFailureScreenViewModelTests.swift | 15 +- ...verConfigurationScreenViewStateTests.swift | 1 + 22 files changed, 129 insertions(+), 184 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 0f0089bc25..5f26dc4552 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,3 +1,3 @@ #!/bin/sh -command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } +command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; } git lfs pre-push "$@" diff --git a/ElementX/Sources/Application/AppCoordinatorProtocol.swift b/ElementX/Sources/Application/AppCoordinatorProtocol.swift index 9cfd3a51cc..232f0ddb15 100644 --- a/ElementX/Sources/Application/AppCoordinatorProtocol.swift +++ b/ElementX/Sources/Application/AppCoordinatorProtocol.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor protocol AppCoordinatorProtocol: CoordinatorProtocol { var windowManager: SecureWindowManagerProtocol { get } diff --git a/ElementX/Sources/Other/Extensions/Snapshotting.swift b/ElementX/Sources/Other/Extensions/Snapshotting.swift index d2a5b13f82..5f106143d2 100644 --- a/ElementX/Sources/Other/Extensions/Snapshotting.swift +++ b/ElementX/Sources/Other/Extensions/Snapshotting.swift @@ -1,17 +1,8 @@ // -// Copyright 2024 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import Combine diff --git a/ElementX/Sources/Other/SwiftUI/ViewModel/BindableState.swift b/ElementX/Sources/Other/SwiftUI/ViewModel/BindableState.swift index 0a90a34ceb..91c061c6f6 100644 --- a/ElementX/Sources/Other/SwiftUI/ViewModel/BindableState.swift +++ b/ElementX/Sources/Other/SwiftUI/ViewModel/BindableState.swift @@ -8,6 +8,7 @@ import Foundation /// Represents a specific portion of the ViewState that can be bound to with SwiftUI's [2-way binding](https://developer.apple.com/documentation/swiftui/binding). +@MainActor protocol BindableState { /// The associated type of the Bindable State. Defaults to Void. associatedtype BindStateType = Void diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift b/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift index 5c91e89e2b..0ec276aeae 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift @@ -5,10 +5,11 @@ // Please see LICENSE files in the repository root for full details. // +import Combine import SwiftUI class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol { - private var dismissalTimer: Timer? + private var timerCancellable: AnyCancellable? private var displayTimes = [String: Date]() private var delayedIndicators = Set() @@ -21,10 +22,11 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol activeIndicator = indicatorQueue.last if let activeIndicator, !activeIndicator.persistent { - dismissalTimer?.invalidate() - dismissalTimer = Timer.scheduledTimer(withTimeInterval: nonPersistentDisplayDuration, repeats: false) { [weak self] _ in + timerCancellable?.cancel() + timerCancellable = Task { [weak self, nonPersistentDisplayDuration] in + try await Task.sleep(for: .seconds(nonPersistentDisplayDuration)) self?.retractIndicatorWithId(activeIndicator.id) - } + }.asCancellable() } } } @@ -46,9 +48,9 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol } else { if let delay { delayedIndicators.insert(indicator.id) - - Timer.scheduledTimer(withTimeInterval: delay.seconds, repeats: false) { [weak self] _ in - guard let self else { return } + + Task { + try await Task.sleep(for: .seconds(delay.seconds)) guard delayedIndicators.contains(indicator.id) else { return @@ -75,10 +77,11 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol indicatorQueue.removeAll { $0.id == id } return } - - Timer.scheduledTimer(withTimeInterval: minimumDisplayDuration, repeats: false) { [weak self] _ in - self?.indicatorQueue.removeAll { $0.id == id } - self?.displayTimes[id] = nil + + Task { + try? await Task.sleep(for: .seconds(minimumDisplayDuration)) + indicatorQueue.removeAll { $0.id == id } + displayTimes[id] = nil } } diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift index 688f7592e5..b675c97a4f 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModelProtocol.swift @@ -7,6 +7,7 @@ import Combine +@MainActor protocol SoftLogoutScreenViewModelProtocol { var actions: AnyPublisher { get } var context: SoftLogoutScreenViewModelType.Context { get } diff --git a/ElementX/Sources/Screens/Authentication/WebRegistrationScreen/View/WebRegistrationScreen.swift b/ElementX/Sources/Screens/Authentication/WebRegistrationScreen/View/WebRegistrationScreen.swift index 9ef81a594f..a45e9e8080 100644 --- a/ElementX/Sources/Screens/Authentication/WebRegistrationScreen/View/WebRegistrationScreen.swift +++ b/ElementX/Sources/Screens/Authentication/WebRegistrationScreen/View/WebRegistrationScreen.swift @@ -81,8 +81,8 @@ struct WebRegistrationWebView: UIViewRepresentable { webView.load(URLRequest(url: url)) } - nonisolated func userContentController(_ userContentController: WKUserContentController, - didReceive message: WKScriptMessage) { + func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else { MXLog.error("Unexpected response.") return @@ -94,7 +94,7 @@ struct WebRegistrationWebView: UIViewRepresentable { } MXLog.info("Received login credentials.") - Task { await viewModelContext.send(viewAction: .signedIn(credentials)) } + viewModelContext.send(viewAction: .signedIn(credentials)) } // MARK: WKUIDelegate @@ -120,8 +120,8 @@ struct WebRegistrationWebView: UIViewRepresentable { // MARK: WKScriptMessageHandler - nonisolated func userContentController(_ userContentController: WKUserContentController, - didReceive message: WKScriptMessage) { + func userContentController(_ userContentController: WKUserContentController, + didReceive message: WKScriptMessage) { coordinator?.userContentController(userContentController, didReceive: message) } } diff --git a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift index 593615f20c..f965e84f55 100644 --- a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift +++ b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift @@ -152,11 +152,8 @@ private struct CallView: UIViewRepresentable { } } - nonisolated func userContentController(_ userContentController: WKUserContentController, - didReceive message: WKScriptMessage) { - Task { @MainActor [weak self] in - self?.viewModelContext?.javaScriptMessageHandler?(message.body) - } + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + viewModelContext?.javaScriptMessageHandler?(message.body) } // MARK: - WKUIDelegate @@ -191,10 +188,8 @@ private struct CallView: UIViewRepresentable { return .cancel } - nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - Task { @MainActor in - viewModelContext?.send(viewAction: .urlChanged(webView.url)) - } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + viewModelContext?.send(viewAction: .urlChanged(webView.url)) } // MARK: - Picture in Picture @@ -271,8 +266,7 @@ private struct CallView: UIViewRepresentable { // MARK: - WKScriptMessageHandler - nonisolated func userContentController(_ userContentController: WKUserContentController, - didReceive message: WKScriptMessage) { + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { coordinator?.userContentController(userContentController, didReceive: message) } } diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModelProtocol.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModelProtocol.swift index 72a0db185f..b995853aba 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModelProtocol.swift @@ -6,7 +6,6 @@ // import Combine -import Foundation @MainActor protocol ReportContentScreenViewModelProtocol { diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift index 8b8073db4d..9f02747cfc 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift @@ -1,17 +1,8 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import Combine diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift index 333d727bd3..d2940f8155 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift @@ -1,17 +1,8 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import Foundation diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift index dd7b454f1d..37e75e264d 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift @@ -1,17 +1,8 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import Combine diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift index 2bdc7bb557..e0e6398173 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift @@ -1,17 +1,8 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import Combine diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift index 4f35ae0955..28e9e67f15 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift @@ -1,17 +1,8 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import Compound diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift index 7b10b069a3..1708888371 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift @@ -9,6 +9,7 @@ import Combine import WysiwygComposer // periphery: ignore - markdown protocol +@MainActor protocol ComposerToolbarViewModelProtocol { var actions: AnyPublisher { get } var context: ComposerToolbarViewModelType.Context { get } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift index c0b50248d0..e20b23b3b6 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModelProtocol.swift @@ -6,8 +6,8 @@ // import Combine -import Foundation +@MainActor protocol RoomScreenViewModelProtocol { var actions: AnyPublisher { get } var context: RoomScreenViewModel.Context { get } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 0c56029c15..8efdfefd12 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -324,16 +324,12 @@ class ClientProxy: ClientProxyProtocol { // Note: This isn't strictly necessary now given the unwrap above, but leaving the code as // documentation. SE-0371 will allow us to fix this by using an async deinit. Task { [syncService] in - do { - defer { - completion?() - } - - try await syncService.stop() - MXLog.info("Sync stopped") - } catch { - MXLog.error("Failed stopping the sync service with error: \(error)") + defer { + completion?() } + + await syncService.stop() + MXLog.info("Sync stopped") } } diff --git a/ElementX/Sources/Services/ComposerDraft/ComposerDraftService.swift b/ElementX/Sources/Services/ComposerDraft/ComposerDraftService.swift index e5410c3d06..b0a5ca1fbb 100644 --- a/ElementX/Sources/Services/ComposerDraft/ComposerDraftService.swift +++ b/ElementX/Sources/Services/ComposerDraft/ComposerDraftService.swift @@ -46,7 +46,7 @@ final class ComposerDraftService: ComposerDraftServiceProtocol { func getReply(eventID: String) async -> Result { switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) { case .success(let replyDetails): - return await .success(timelineItemfactory.buildReply(details: replyDetails)) + return .success(timelineItemfactory.buildReply(details: replyDetails)) case .failure(let error): MXLog.error("Could not load reply: \(error)") return .failure(.failedToLoadReply) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 063d373ea0..1ec917668a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -62,8 +62,6 @@ class TimelineController: TimelineControllerProtocol { activeTimeline = timelineProxy activeTimelineProvider = liveTimelineProvider - NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) - guard let initialFocussedEventID else { configureActiveTimelineProvider() return @@ -148,7 +146,9 @@ class TimelineController: TimelineControllerProtocol { } if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol { - fetchEventDetails(for: messageTimelineItem, refetchOnError: true) + fetchEventDetails(for: messageTimelineItem, + refetchOnError: true, + activeTimeline: activeTimeline) } } @@ -375,55 +375,60 @@ class TimelineController: TimelineControllerProtocol { paginationState = PaginationState(backward: .paginating, forward: .paginating) callbacks.send(.isLive(activeTimelineProvider.kind == .live)) - updateTimelineItemsCancellable = activeTimelineProvider - .updatePublisher - .receive(on: serialDispatchQueue) - .sink { [weak self] items, paginationState in - self?.updateTimelineItems(itemProxies: items, paginationState: paginationState) + updateTimelineItemsCancellable = Task { [weak self, activeTimelineProvider] in + let contentSizeChangePublisher = NotificationCenter.default.publisher(for: UIContentSizeCategory.didChangeNotification) + let timelineUpdates = activeTimelineProvider.updatePublisher.merge(with: contentSizeChangePublisher.map { _ in + (activeTimelineProvider.itemProxies, activeTimelineProvider.paginationState) + }) + + for await (items, paginationState) in timelineUpdates.values { + await self?.updateTimelineItems(itemProxies: items, paginationState: paginationState) } + }.asCancellable() } - @objc private func contentSizeCategoryDidChange() { - // Recompute all attributed strings on content size changes -> DynamicType support - serialDispatchQueue.async { [activeTimelineProvider] in - self.updateTimelineItems(itemProxies: activeTimelineProvider.itemProxies, paginationState: activeTimelineProvider.paginationState) - } - } - - private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: PaginationState) { - var newTimelineItems = [RoomTimelineItemProtocol]() - + private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: PaginationState) async { let isNewTimeline = isSwitchingTimelines isSwitchingTimelines = false - let collapsibleChunks = itemProxies.groupBy { isItemCollapsible($0) } + let isDM = roomProxy.isDirectOneToOneRoom - for (index, collapsibleChunk) in collapsibleChunks.enumerated() { - let isLastItem = index == collapsibleChunks.indices.last + var newTimelineItems = await Task.detached { [timelineItemFactory, activeTimeline] in + var newTimelineItems = [RoomTimelineItemProtocol]() - let items = collapsibleChunk.compactMap { itemProxy in + let collapsibleChunks = itemProxies.groupBy { $0.isItemCollapsible } + + for (index, collapsibleChunk) in collapsibleChunks.enumerated() { + let isLastItem = index == collapsibleChunks.indices.last - let timelineItem = buildTimelineItem(for: itemProxy) + let items = collapsibleChunk.compactMap { itemProxy in + let timelineItem = self.buildTimelineItem(for: itemProxy, + isDM: isDM, + timelineItemFactory: timelineItemFactory, + activeTimeline: activeTimeline) + + return timelineItem + } - return timelineItem - } - - if items.isEmpty { - continue - } - - if items.count == 1, let timelineItem = items.first { - // Don't show the read marker if it's the last item in the timeline - // https://github.com/matrix-org/matrix-rust-sdk/issues/1546 - guard !(timelineItem is ReadMarkerRoomTimelineItem && isLastItem) else { + if items.isEmpty { continue } - newTimelineItems.append(timelineItem) - } else { - newTimelineItems.append(CollapsibleTimelineItem(items: items)) + if items.count == 1, let timelineItem = items.first { + // Don't show the read marker if it's the last item in the timeline + // https://github.com/matrix-org/matrix-rust-sdk/issues/1546 + guard !(timelineItem is ReadMarkerRoomTimelineItem && isLastItem) else { + continue + } + + newTimelineItems.append(timelineItem) + } else { + newTimelineItems.append(CollapsibleTimelineItem(items: items)) + } } - } + + return newTimelineItems + }.value // Check if we need to add anything to the top of the timeline. switch paginationState.backward { @@ -445,23 +450,26 @@ class TimelineController: TimelineControllerProtocol { break } - DispatchQueue.main.sync { - timelineItems = newTimelineItems - } + timelineItems = newTimelineItems callbacks.send(.updatedTimelineItems(timelineItems: newTimelineItems, isSwitchingTimelines: isNewTimeline)) self.paginationState = paginationState } - private func buildTimelineItem(for itemProxy: TimelineItemProxy) -> RoomTimelineItemProtocol? { + private nonisolated func buildTimelineItem(for itemProxy: TimelineItemProxy, + isDM: Bool, + timelineItemFactory: RoomTimelineItemFactoryProtocol, + activeTimeline: TimelineProxyProtocol) -> RoomTimelineItemProtocol? { switch itemProxy { case .event(let eventTimelineItem): - let timelineItem = timelineItemFactory.buildTimelineItem(for: eventTimelineItem, isDM: roomProxy.isDirectOneToOneRoom) - + let timelineItem = timelineItemFactory.buildTimelineItem(for: eventTimelineItem, isDM: isDM) + if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol { // Avoid fetching this over and over again as it changes states if it keeps failing to load // Errors will be handled again on appearance - fetchEventDetails(for: messageTimelineItem, refetchOnError: false) + fetchEventDetails(for: messageTimelineItem, + refetchOnError: false, + activeTimeline: activeTimeline) } return timelineItem @@ -477,21 +485,10 @@ class TimelineController: TimelineControllerProtocol { return nil } } - - private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool { - if case let .event(eventItem) = item { - switch eventItem.content { - case .profileChange, .roomMembership, .state: - return true - default: - return false - } - } - - return false - } - private func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, refetchOnError: Bool) { + private nonisolated func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, + refetchOnError: Bool, + activeTimeline: TimelineProxyProtocol) { guard let eventID = timelineItem.id.eventID else { return } @@ -524,3 +521,18 @@ class TimelineController: TimelineControllerProtocol { return nil } } + +private extension TimelineItemProxy { + var isItemCollapsible: Bool { + if case let .event(eventItem) = self { + switch eventItem.content { + case .profileChange, .roomMembership, .state: + return true + default: + return false + } + } + + return false + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift index 64129694ae..1065f51fdd 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactoryProtocol.swift @@ -9,7 +9,6 @@ import Foundation import MatrixRustSDK -@MainActor protocol RoomTimelineItemFactoryProtocol { func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol? func buildReply(details: InReplyToDetails) -> TimelineItemReply diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift index 6b738a65c6..d7834f7fc6 100644 --- a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift @@ -1,17 +1,8 @@ // -// Copyright 2022 New Vector Ltd +// Copyright 2022-2024 New Vector Ltd. // -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. // import XCTest diff --git a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift index 1b8188cf1a..90a92efdb7 100644 --- a/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift +++ b/UnitTests/Sources/ServerConfigurationScreenViewStateTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import ElementX +@MainActor class ServerConfirmationScreenViewStateTests: XCTestCase { func testLoginMessageString() { let matrixDotOrgLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address,