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
@@ -10,7 +10,7 @@ import CoreLocation
import Foundation
/// A structure that parses a geo URI (i.e. geo:53.99803101552848,-8.25347900390625;u=10) and constructs their constituent parts.
struct GeoURI: Hashable {
nonisolated struct GeoURI: Hashable {
// MARK: - Properties
let latitude: Double
@@ -69,8 +69,11 @@ struct GeoURI: Hashable {
// swiftlint:disable:next large_tuple
private typealias RegexGeoURI = Regex<(Substring, latitude: Substring, longitude: Substring, uncertainty: Substring?)>
private extension RegexGeoURI {
static let standard: Self = /geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
private nonisolated extension RegexGeoURI {
/// A computed property as `Regex` isn't Sendable.
static var standard: Self {
/geo:(?<latitude>-?\d+(?:\.\d+)?),(?<longitude>-?\d+(?:\.\d+)?)(?:,-?\d+(?:\.\d+)?)?(?:;u=(?<uncertainty>\d+(?:\.\d+)?))?/
}
}
extension GeoURI {
@@ -79,7 +82,7 @@ extension GeoURI {
}
}
private extension NumberFormatter {
private nonisolated extension NumberFormatter {
static let decimal: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.locale = Locale(identifier: "en_US_POSIX")
@@ -574,10 +574,14 @@ class TimelineController: TimelineControllerProtocol {
switch timelineItem.properties.replyDetails {
case .notLoaded:
activeTimeline.fetchDetails(for: eventID)
Task { @MainActor in
activeTimeline.fetchDetails(for: eventID)
}
case .error:
if refetchOnError {
activeTimeline.fetchDetails(for: eventID)
Task { @MainActor in
activeTimeline.fetchDetails(for: eventID)
}
}
default:
break
@@ -653,7 +657,7 @@ class TimelineController: TimelineControllerProtocol {
}
}
private extension TimelineItemProxy {
private nonisolated extension TimelineItemProxy {
var isItemCollapsible: Bool {
if case let .event(eventItem) = self {
switch eventItem.content {
@@ -13,7 +13,6 @@ enum TimelineFactoryControllerError: Error {
case roomProxyError(RoomProxyError)
}
@MainActor
protocol TimelineControllerFactoryProtocol {
func buildTimelineController(roomProxy: JoinedRoomProxyProtocol,
initialFocussedEventID: String?,
@@ -9,7 +9,7 @@
import Foundation
/// Represents all reactions of the same type for a single event.
struct AggregatedReaction: Hashable, Identifiable {
nonisolated struct AggregatedReaction: Hashable, Identifiable {
/// Length at which we ellipsize a reaction key for display
/// Reactions can be free text, so we need to limit the length
/// displayed on screen.
@@ -28,14 +28,14 @@ struct AggregatedReaction: Hashable, Identifiable {
}
/// Details of who sent the reaction
struct ReactionSender: Hashable, Identifiable {
nonisolated struct ReactionSender: Hashable, Identifiable {
/// The id of the user who sent the reaction
let id: String
/// The time that the reaction was received on the original homeserver
let timestamp: Date
}
extension AggregatedReaction {
nonisolated extension AggregatedReaction {
/// The number of times this reactions was sent.
var count: Int {
senders.count
@@ -13,7 +13,7 @@ import SwiftUI
/// Represents an issue with a timeline item's authenticity such as coming from an
/// unsigned session or being sent unencrypted in an encrypted room. See Rust's
/// `ShieldStateCode` for more information about the meaning of the cases.
enum EncryptionAuthenticity: Hashable {
nonisolated enum EncryptionAuthenticity: Hashable {
enum Color { case red, gray }
case notGuaranteed(color: Color)
@@ -65,7 +65,7 @@ enum EncryptionAuthenticity: Hashable {
}
}
extension EncryptionAuthenticity {
nonisolated extension EncryptionAuthenticity {
init?(shieldState: ShieldState) {
switch shieldState {
case .red(let code):
@@ -8,12 +8,12 @@
import Foundation
struct ReadReceipt: Hashable {
nonisolated struct ReadReceipt: Hashable {
let userID: String
let formattedTimestamp: String?
}
extension ReadReceipt: Identifiable {
nonisolated extension ReadReceipt: Identifiable {
var id: String {
userID
}
@@ -9,7 +9,7 @@
import Foundation
/// Properties of a matrix event that are common between all timeline items.
struct RoomTimelineItemProperties: Hashable {
nonisolated struct RoomTimelineItemProperties: Hashable {
/// Information on the item this one replying to
var replyDetails: TimelineItemReplyDetails?
/// Whether it's part of a thread or not
@@ -8,7 +8,7 @@
import Foundation
enum TimelineEventContent: Hashable {
nonisolated enum TimelineEventContent: Hashable {
case message(EventBasedMessageTimelineItemContentType)
case poll(question: String)
case liveLocation
@@ -8,7 +8,7 @@
import MatrixRustSDK
import SwiftUI
struct TimelineItemKeyForwarder: Identifiable, Hashable {
nonisolated struct TimelineItemKeyForwarder: Identifiable, Hashable {
let id: String
let displayName: String?
@@ -8,12 +8,12 @@
import Foundation
struct TimelineItemReply {
nonisolated struct TimelineItemReply {
let details: TimelineItemReplyDetails
let isThreaded: Bool
}
enum TimelineItemReplyDetails: Hashable {
nonisolated enum TimelineItemReplyDetails: Hashable {
case notLoaded(eventID: String)
case loading(eventID: String)
case loaded(sender: TimelineItemSender, eventID: String, eventContent: TimelineEventContent)
@@ -6,7 +6,7 @@
// Please see LICENSE files in the repository root for full details.
//
enum TimelineItemThreadSummary: Hashable {
nonisolated enum TimelineItemThreadSummary: Hashable {
case notLoaded
case loading
case loaded(senderID: String, sender: TimelineItemSender, latestEventContent: TimelineEventContent, numberOfReplies: Int)
@@ -14,7 +14,7 @@ import Foundation
/// Its value is consistent only per timeline instance, it should **not** be used to identify an item across timeline instances.
/// - eventOrTransactionID: Contains the 2 possible identifiers of an event, either it has a remote event id or
/// a local transaction id, never both or none.
enum TimelineItemIdentifier: Hashable {
nonisolated enum TimelineItemIdentifier: Hashable {
struct UniqueID: Hashable {
let value: String
@@ -77,7 +77,7 @@ enum TimelineItemIdentifier: Hashable {
// MARK: - Mocks
extension TimelineItemIdentifier {
nonisolated extension TimelineItemIdentifier {
static var randomEvent: Self {
.event(uniqueID: .init(UUID().uuidString), eventOrTransactionID: .eventID(UUID().uuidString))
}
@@ -68,7 +68,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
// The stream also allows to handle the sendable items in the listener.
Task { [weak self] in
for await diffs in diffStream {
self?.updateItemsWithDiffs(diffs)
await self?.applyDiffs(diffs)
}
}
@@ -81,17 +81,34 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
// MARK: - Private
private func updateItemsWithDiffs(_ diffs: [TimelineDiff]) {
let span = MXLog.createSpan("process_timeline_list_diffs:\(kind)")
private func applyDiffs(_ diffs: [TimelineDiff]) async {
MXLog.verbose("Received diffs: \(diffs)")
// Building the item proxies and computing/applying the CollectionDifference can be
// expensive, so run it off the main actor and only hop back to publish the result.
let result = await Self.processDiffs(diffs, on: itemProxies, spanName: "process_timeline_list_diffs:\(kind)")
itemProxies = result.itemProxies
if result.hasMembershipChange {
membershipChangeSubject.send(())
}
}
@concurrent
private static func processDiffs(_ diffs: [TimelineDiff],
on currentItems: [TimelineItemProxy],
spanName: String) async -> (itemProxies: [TimelineItemProxy], hasMembershipChange: Bool) {
let span = MXLog.createSpan(spanName)
span.enter()
defer {
span.exit()
}
MXLog.verbose("Received diffs: \(diffs)")
var hasMembershipChange = false
itemProxies = diffs.reduce(itemProxies) { currentItems, diff in
guard let collectionDiff = buildDiff(from: diff, on: currentItems) else {
let itemProxies = diffs.reduce(currentItems) { currentItems, diff in
guard let collectionDiff = buildDiff(from: diff, on: currentItems, hasMembershipChange: &hasMembershipChange) else {
MXLog.error("Failed building CollectionDifference from \(diff)")
return currentItems
}
@@ -103,9 +120,11 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
return updatedItems
}
return (itemProxies, hasMembershipChange)
}
private func buildDiff(from diff: TimelineDiff, on itemProxies: [TimelineItemProxy]) -> CollectionDifference<TimelineItemProxy>? {
private nonisolated static func buildDiff(from diff: TimelineDiff, on itemProxies: [TimelineItemProxy], hasMembershipChange: inout Bool) -> CollectionDifference<TimelineItemProxy>? {
var changes = [CollectionDifference<TimelineItemProxy>.Change]()
switch diff {
@@ -114,7 +133,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
let itemProxy = TimelineItemProxy(item: item)
if itemProxy.isMembershipChange {
membershipChangeSubject.send(())
hasMembershipChange = true
}
changes.append(.insert(offset: Int(itemProxies.count) + index, element: itemProxy, associatedWith: nil))
@@ -138,7 +157,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
let itemProxy = TimelineItemProxy(item: item)
if itemProxy.isMembershipChange {
membershipChangeSubject.send(())
hasMembershipChange = true
}
changes.append(.insert(offset: Int(itemProxies.count), element: itemProxy, associatedWith: nil))
@@ -170,7 +189,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
}
}
private extension TimelineItemProxy {
private nonisolated extension TimelineItemProxy {
var isMembershipChange: Bool {
switch self {
case .event(let eventTimelineItemProxy):
@@ -10,10 +10,10 @@ import Combine
import Foundation
import MatrixRustSDK
struct TimelinePaginationState: Equatable {
nonisolated struct TimelinePaginationState: Equatable {
/// An initial state that is used to prevent pagination whilst loading the timeline.
/// Once the initial items are loaded the TimelineProxy will publish the correct value.
static var initial = TimelinePaginationState(backward: .endReached, forward: .endReached)
static let initial = TimelinePaginationState(backward: .endReached, forward: .endReached)
let backward: PaginationState
let forward: PaginationState
}
@@ -21,7 +21,6 @@ struct TimelinePaginationState: Equatable {
/// Entities implementing this protocol are responsible for processings diffs coming from the rust timeline
/// and converting them into an array of Element X specific ``TimelineItemProxy``s that will be
/// published as an array together with the pagination state through the ``updatePublisher``.
@MainActor
protocol TimelineItemProviderProtocol {
/// A publisher that signals when ``itemProxies`` or ``paginationState`` are changed.
var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> { get }
@@ -11,7 +11,7 @@ import MatrixRustSDK
import MatrixRustSDKMocks
/// A light wrapper around timeline items returned from Rust.
enum TimelineItemProxy {
nonisolated enum TimelineItemProxy {
case event(EventTimelineItemProxy)
case virtual(MatrixRustSDK.VirtualTimelineItem, uniqueID: TimelineItemIdentifier.UniqueID)
case unknown(MatrixRustSDK.TimelineItem)
@@ -37,7 +37,7 @@ enum TimelineItemProxy {
}
/// The delivery status for the item.
enum TimelineItemDeliveryStatus: Hashable {
nonisolated enum TimelineItemDeliveryStatus: Hashable {
case sending
case sent
case sendingFailed(TimelineItemSendFailure)
@@ -51,7 +51,7 @@ enum TimelineItemDeliveryStatus: Hashable {
}
/// The reason a timeline item failed to send.
enum TimelineItemSendFailure: Hashable {
nonisolated enum TimelineItemSendFailure: Hashable {
enum VerifiedUser: Hashable {
case hasUnsignedDevice(devices: [String: [String]])
case changedIdentity(users: [String])
@@ -69,7 +69,11 @@ enum TimelineItemSendFailure: Hashable {
}
/// A light wrapper around event timeline items returned from Rust.
class EventTimelineItemProxy {
///
/// The proxy is shared between the main actor and the background timeline item
/// building, so it's stateless and forwards everything to the underlying
/// (internally synchronised) Rust object.
final nonisolated class EventTimelineItemProxy: Sendable {
let item: MatrixRustSDK.EventTimelineItem
let id: TimelineItemIdentifier
@@ -79,13 +83,13 @@ class EventTimelineItemProxy {
id = .event(uniqueID: uniqueID, eventOrTransactionID: .init(rustValue: item.eventOrTransactionId))
}
lazy var deliveryStatus: TimelineItemDeliveryStatus? = {
var deliveryStatus: TimelineItemDeliveryStatus? {
guard let localSendState = item.localSendState else {
return nil
}
switch localSendState {
case .sendingFailed(let error, let isRecoverable):
case .sendingFailed(let error, _):
switch error {
case .identityViolations(let users):
return .sendingFailed(.verifiedUser(.changedIdentity(users: users)))
@@ -99,42 +103,62 @@ class EventTimelineItemProxy {
case .sent:
return .sent
}
}()
}
lazy var canBeRepliedTo = item.canBeRepliedTo
var canBeRepliedTo: Bool {
item.canBeRepliedTo
}
lazy var content = item.content
var content: TimelineItemContent {
item.content
}
lazy var isOwn = item.isOwn
var isOwn: Bool {
item.isOwn
}
lazy var isEditable = item.isEditable
var isEditable: Bool {
item.isEditable
}
lazy var sender = TimelineItemSender(senderID: item.sender, senderProfile: item.senderProfile)
var sender: TimelineItemSender {
TimelineItemSender(senderID: item.sender, senderProfile: item.senderProfile)
}
lazy var forwarder: TimelineItemKeyForwarder? = {
var forwarder: TimelineItemKeyForwarder? {
guard let forwarderID = item.forwarder, let forwarderProfile = item.forwarderProfile else {
return nil
}
return TimelineItemKeyForwarder(forwarderID: forwarderID, forwarderProfile: forwarderProfile)
}()
}
lazy var timestamp = Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000))
var timestamp: Date {
Date(timeIntervalSince1970: TimeInterval(item.timestamp / 1000))
}
lazy var debugInfo: TimelineItemDebugInfo = {
var debugInfo: TimelineItemDebugInfo {
let debugInfo = item.lazyProvider.debugInfo()
return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson)
}()
}
lazy var shieldState = item.lazyProvider.getShields(strict: false)
var shieldState: ShieldState? {
item.lazyProvider.getShields(strict: false)
}
lazy var sendHandle = item.lazyProvider.getSendHandle()
var sendHandle: SendHandle? {
item.lazyProvider.getSendHandle()
}
lazy var shouldBoost = item.lazyProvider.containsOnlyEmojis()
var shouldBoost: Bool {
item.lazyProvider.containsOnlyEmojis()
}
lazy var readReceipts = item.readReceipts
var readReceipts: [String: Receipt] {
item.readReceipts
}
}
struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
nonisolated struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
let id = UUID()
let model: String
let originalJSON: String?
@@ -175,7 +199,7 @@ struct TimelineItemDebugInfo: Identifiable, CustomStringConvertible {
}
}
struct SendHandleProxy: Hashable {
nonisolated struct SendHandleProxy: Hashable {
enum Error: Swift.Error {
case sdkError(Swift.Error)
}
@@ -209,7 +233,7 @@ struct SendHandleProxy: Hashable {
}
}
struct VideoInfoProxy: Hashable {
nonisolated struct VideoInfoProxy: Hashable {
let source: MediaSourceProxy
private(set) var duration: TimeInterval
private(set) var size: CGSize?
@@ -253,7 +277,7 @@ struct VideoInfoProxy: Hashable {
}
}
struct ImageInfoProxy: Hashable {
nonisolated struct ImageInfoProxy: Hashable {
let source: MediaSourceProxy
private(set) var size: CGSize?
private(set) var aspectRatio: CGFloat?
@@ -317,7 +341,7 @@ struct ImageInfoProxy: Hashable {
}
}
private struct MediaInfoProxy: Hashable {
private nonisolated struct MediaInfoProxy: Hashable {
private(set) var size: CGSize?
private(set) var mimeType: String?
private(set) var aspectRatio: CGFloat?
@@ -9,7 +9,7 @@
import MatrixRustSDK
import SwiftUI
struct TimelineItemSender: Identifiable, Hashable {
nonisolated struct TimelineItemSender: Identifiable, Hashable {
static let test = TimelineItemSender(id: "@test.matrix.org")
let id: String
@@ -9,4 +9,4 @@
import Foundation
// periphery: ignore - markdown protocol
protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol { }
nonisolated protocol DecorationTimelineItemProtocol: RoomTimelineItemProtocol { }
@@ -8,7 +8,7 @@
import Foundation
enum EventBasedMessageTimelineItemContentType: Hashable {
nonisolated enum EventBasedMessageTimelineItemContentType: Hashable {
case audio(AudioRoomTimelineItemContent)
case emote(EmoteRoomTimelineItemContent)
case file(FileRoomTimelineItemContent)
@@ -20,11 +20,11 @@ enum EventBasedMessageTimelineItemContentType: Hashable {
case voice(AudioRoomTimelineItemContent)
}
protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
nonisolated protocol EventBasedMessageTimelineItemProtocol: EventBasedTimelineItemProtocol {
var contentType: EventBasedMessageTimelineItemContentType { get }
}
extension EventBasedMessageTimelineItemProtocol {
nonisolated extension EventBasedMessageTimelineItemProtocol {
var supportsMediaCaption: Bool {
switch contentType {
case .audio, .file, .image, .video:
@@ -9,7 +9,7 @@
import Foundation
import UIKit
protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
nonisolated protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringConvertible {
var timestamp: Date { get }
var isOutgoing: Bool { get }
var isEditable: Bool { get }
@@ -22,7 +22,7 @@ protocol EventBasedTimelineItemProtocol: RoomTimelineItemProtocol, CustomStringC
var properties: RoomTimelineItemProperties { get }
}
extension EventBasedTimelineItemProtocol {
nonisolated extension EventBasedTimelineItemProtocol {
var description: String {
"\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)"
}
@@ -8,7 +8,7 @@
import Foundation
struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -9,7 +9,7 @@
import UIKit
import UniformTypeIdentifiers
struct AudioRoomTimelineItemContent: Hashable {
nonisolated struct AudioRoomTimelineItemContent: Hashable {
let filename: String
var caption: String?
var formattedCaption: AttributedString?
@@ -8,7 +8,7 @@
import UIKit
struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
nonisolated struct EmoteRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -8,7 +8,7 @@
import UIKit
struct EmoteRoomTimelineItemContent: Hashable {
nonisolated struct EmoteRoomTimelineItemContent: Hashable {
let body: String
var formattedBody: AttributedString?
/// The original textual representation of the formatted body directly from the event (usually HTML code)
@@ -9,7 +9,7 @@
import UIKit
import UniformTypeIdentifiers
struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct FileRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -9,7 +9,7 @@
import Foundation
import UniformTypeIdentifiers
struct FileRoomTimelineItemContent: Hashable {
nonisolated struct FileRoomTimelineItemContent: Hashable {
let filename: String
var caption: String?
var formattedCaption: AttributedString?
@@ -9,7 +9,7 @@
import UIKit
import UniformTypeIdentifiers
struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct ImageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -9,7 +9,7 @@
import Foundation
import UniformTypeIdentifiers
struct ImageRoomTimelineItemContent: Hashable {
nonisolated struct ImageRoomTimelineItemContent: Hashable {
let filename: String
var caption: String?
var formattedCaption: AttributedString?
@@ -8,7 +8,7 @@
import Foundation
import MatrixRustSDK
struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
nonisolated struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
/// Always empty just here for protocol conformance
var body: String {
@@ -25,14 +25,14 @@ struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
var properties = RoomTimelineItemProperties()
}
struct LiveLocationRoomTimelineItemContent: Equatable {
nonisolated struct LiveLocationRoomTimelineItemContent: Equatable {
let isLive: Bool
let timeoutDate: Date
let lastGeoURI: GeoURI?
}
extension LiveLocationRoomTimelineItemContent {
nonisolated extension LiveLocationRoomTimelineItemContent {
init(from content: MatrixRustSDK.LiveLocationContent, timestamp: Date) {
isLive = content.isLive
timeoutDate = timestamp.addingTimeInterval(Double(content.timeoutMs) / 1000)
@@ -8,7 +8,7 @@
import Foundation
struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -8,7 +8,7 @@
import MatrixRustSDK
struct LocationRoomTimelineItemContent: Hashable {
nonisolated struct LocationRoomTimelineItemContent: Hashable {
let body: String
let geoURI: GeoURI?
let kind: StaticLocationKind
@@ -22,7 +22,7 @@ struct LocationRoomTimelineItemContent: Hashable {
}
}
enum StaticLocationKind {
nonisolated enum StaticLocationKind {
case sender
case pin
@@ -8,7 +8,7 @@
import UIKit
struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
nonisolated struct NoticeRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -8,7 +8,7 @@
import UIKit
struct NoticeRoomTimelineItemContent: Hashable {
nonisolated struct NoticeRoomTimelineItemContent: Hashable {
let body: String
var formattedBody: AttributedString?
}
@@ -9,7 +9,7 @@
import Algorithms
import UIKit
struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
nonisolated struct TextRoomTimelineItem: TextBasedRoomTimelineItem, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -8,7 +8,7 @@
import UIKit
struct TextRoomTimelineItemContent: Hashable {
nonisolated struct TextRoomTimelineItemContent: Hashable {
let body: String
var formattedBody: AttributedString?
/// The original textual representation of the formatted body directly from the event (usually HTML code)
@@ -9,7 +9,7 @@
import UIKit
import UniformTypeIdentifiers
struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -9,7 +9,7 @@
import Foundation
import UniformTypeIdentifiers
struct VideoRoomTimelineItemContent: Hashable {
nonisolated struct VideoRoomTimelineItemContent: Hashable {
let filename: String
var caption: String?
var formattedCaption: AttributedString?
@@ -8,7 +8,7 @@
import Foundation
struct VoiceMessageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
nonisolated struct VoiceMessageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isOutgoing: Bool
@@ -8,7 +8,7 @@
import Foundation
struct CallInviteRoomTimelineItem: RoomTimelineItemProtocol, Equatable {
nonisolated struct CallInviteRoomTimelineItem: RoomTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isEditable: Bool
@@ -8,7 +8,7 @@
import Foundation
struct CallNotificationRoomTimelineItem: RoomTimelineItemProtocol, Equatable {
nonisolated struct CallNotificationRoomTimelineItem: RoomTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
let isEditable: Bool
@@ -8,7 +8,7 @@
import Foundation
struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Equatable {
nonisolated struct CollapsibleTimelineItem: RoomTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let items: [RoomTimelineItemProtocol]
let itemIDs: [TimelineItemIdentifier]
@@ -8,7 +8,7 @@
import UIKit
struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
nonisolated struct EncryptedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
enum EncryptionType: Hashable {
case megolmV1AesSha2(sessionID: String, cause: UTDCause)
case olmV1Curve25519AesSha2(senderKey: String)
@@ -8,7 +8,7 @@
import Foundation
struct PollRoomTimelineItem: Equatable, EventBasedTimelineItemProtocol {
nonisolated struct PollRoomTimelineItem: Equatable, EventBasedTimelineItemProtocol {
let id: TimelineItemIdentifier
let poll: Poll
let body: String
@@ -20,7 +20,7 @@ struct PollRoomTimelineItem: Equatable, EventBasedTimelineItemProtocol {
var properties: RoomTimelineItemProperties
}
struct Poll: Hashable {
nonisolated struct Poll: Hashable {
let question: String
let kind: Kind
let maxSelections: Int
@@ -9,7 +9,7 @@
import Foundation
import UIKit
struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
nonisolated struct RedactedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let timestamp: Date
@@ -8,7 +8,7 @@
import UIKit
struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
nonisolated struct StateRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let timestamp: Date
@@ -8,7 +8,7 @@
import UIKit
struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
nonisolated struct StickerRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
let timestamp: Date
@@ -8,7 +8,7 @@
import UIKit
struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
nonisolated struct UnsupportedRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let body: String
@@ -8,7 +8,7 @@
import Foundation
struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
nonisolated struct PaginationIndicatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
enum Position {
@@ -8,6 +8,6 @@
import Foundation
struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
nonisolated struct ReadMarkerRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
}
@@ -8,7 +8,7 @@
import Foundation
struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
nonisolated struct SeparatorRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
let id: TimelineItemIdentifier
let timestamp: Date
}
@@ -8,7 +8,7 @@
import Foundation
struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
nonisolated struct TimelineStartRoomTimelineItem: DecorationTimelineItemProtocol, Equatable {
// Using a static identifier makes the animations consistent in SwiftUI
let id: TimelineItemIdentifier = .virtual(uniqueID: .init("TimelineStart"))
let name: String?
@@ -9,7 +9,7 @@
import MatrixRustSDK
import UIKit
struct RoomStateEventStringBuilder {
nonisolated struct RoomStateEventStringBuilder {
let userID: String
func buildString(for change: MembershipChange?,
@@ -10,7 +10,7 @@ import MatrixRustSDK
import UIKit
import UniformTypeIdentifiers
struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
nonisolated struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
private let attributedStringBuilder: AttributedStringBuilderProtocol
private let stateEventStringBuilder: RoomStateEventStringBuilder
@@ -927,7 +927,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
}
private extension EmbeddedEventDetails {
private nonisolated extension EmbeddedEventDetails {
var isThreaded: Bool {
switch self {
case .ready(.msgLike(let messageLikeContent), _, _, _, _):
@@ -938,7 +938,7 @@ private extension EmbeddedEventDetails {
}
}
private extension Receipt {
private nonisolated extension Receipt {
var dateTimestamp: Date? {
guard let timestamp else {
return nil
@@ -9,7 +9,7 @@
import Foundation
import MatrixRustSDK
protocol RoomTimelineItemFactoryProtocol {
nonisolated protocol RoomTimelineItemFactoryProtocol: Sendable {
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
func buildTimelineItemReply(_ details: MatrixRustSDK.InReplyToDetails) -> TimelineItemReply
}
@@ -9,11 +9,11 @@
import Foundation
import UIKit
protocol RoomTimelineItemProtocol {
nonisolated protocol RoomTimelineItemProtocol: Sendable {
var id: TimelineItemIdentifier { get }
}
extension RoomTimelineItemProtocol {
nonisolated extension RoomTimelineItemProtocol {
var isReactable: Bool {
guard let eventItem = self as? EventBasedTimelineItemProtocol else { return false }
return !eventItem.isRedacted && !eventItem.hasFailedToSend && !eventItem.hasFailedDecryption
@@ -8,4 +8,4 @@
import Foundation
protocol TextBasedRoomTimelineItem: EventBasedMessageTimelineItemProtocol { }
nonisolated protocol TextBasedRoomTimelineItem: EventBasedMessageTimelineItemProtocol { }
@@ -31,12 +31,32 @@ final class TimelineProxy: TimelineProxyProtocol {
}
deinit {
backPaginationStatusContinuation.finish()
backPaginationStateObservationToken?.cancel()
}
/// Bridge from the SDK's synchronous callback into Swift Concurrency. Yielding is safe from any
/// thread; a single long-lived `for await` consumer (set up in `init`) applies the status updates
/// on the main actor in FIFO order.
private let backPaginationStatusContinuation: AsyncStream<PaginationStatus>.Continuation
init(timeline: Timeline, kind: TimelineKind) {
self.timeline = timeline
self.kind = kind
let (backPaginationStatusStream, backPaginationStatusContinuation) = AsyncStream<PaginationStatus>.makeStream()
self.backPaginationStatusContinuation = backPaginationStatusContinuation
Task { [weak self] in
for await status in backPaginationStatusStream {
switch status {
case .idle(let hitStartOfTimeline):
self?.backPaginationStateSubject.send(hitStartOfTimeline ? .endReached : .idle)
case .paginating:
self?.backPaginationStateSubject.send(.paginating)
}
}
}
}
func subscribeForUpdates() async {
@@ -52,7 +72,7 @@ final class TimelineProxy: TimelineProxyProtocol {
await subscribeToPagination()
let provider = await TimelineItemProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher)
let provider = TimelineItemProvider(timeline: timeline, kind: kind, paginationStatePublisher: paginationStatePublisher)
innerTimelineItemProvider = provider
@@ -74,7 +94,7 @@ final class TimelineProxy: TimelineProxyProtocol {
}
func messageEventContent(for timelineItemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation? {
guard let content = await timelineItemProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content,
guard let content = timelineItemProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content,
case let .msgLike(messageLikeContent) = content,
case let .message(messageContent) = messageLikeContent.kind else {
return nil
@@ -236,7 +256,7 @@ final class TimelineProxy: TimelineProxyProtocol {
inReplyTo: nil),
audioInfo: audioInfo)
await requestHandle(handle)
requestHandle(handle)
try await handle.join()
MXLog.info("Finished sending audio")
@@ -262,7 +282,7 @@ final class TimelineProxy: TimelineProxyProtocol {
inReplyTo: nil),
fileInfo: fileInfo)
await requestHandle(handle)
requestHandle(handle)
try await handle.join()
MXLog.info("Finished sending file")
@@ -290,7 +310,7 @@ final class TimelineProxy: TimelineProxyProtocol {
thumbnailSource: .file(filename: thumbnailURL.path(percentEncoded: false)),
imageInfo: imageInfo)
await requestHandle(handle)
requestHandle(handle)
try await handle.join()
MXLog.info("Finished sending image")
@@ -342,7 +362,7 @@ final class TimelineProxy: TimelineProxyProtocol {
thumbnailSource: .file(filename: thumbnailURL.path(percentEncoded: false)),
videoInfo: videoInfo)
await requestHandle(handle)
requestHandle(handle)
try await handle.join()
MXLog.info("Finished sending video")
@@ -369,7 +389,7 @@ final class TimelineProxy: TimelineProxyProtocol {
audioInfo: audioInfo,
waveform: waveform)
await requestHandle(handle)
requestHandle(handle)
try await handle.join()
MXLog.info("Finished sending voice message")
@@ -582,17 +602,8 @@ final class TimelineProxy: TimelineProxyProtocol {
private func subscribeToPagination() async {
switch kind {
case .live:
let backPaginationListener = SDKListener<PaginationStatus> { [weak self] status in
guard let self else {
return
}
switch status {
case .idle(let hitStartOfTimeline):
backPaginationStateSubject.send(hitStartOfTimeline ? .endReached : .idle)
case .paginating:
backPaginationStateSubject.send(.paginating)
}
let backPaginationListener = SDKListener<PaginationStatus> { [backPaginationStatusContinuation] status in
backPaginationStatusContinuation.yield(status)
}
do {
@@ -53,7 +53,7 @@ enum TimelineProxyError: Error {
/// Element X proxies generally wrap the counterpart RustSDK objects while providing platform specific
/// interfaces. In this case it composes methods for interacting with a room's timeline and should be used alongside
/// the ``TimelineItemProviderProtocol`` which offers a reactive interface to timeline changes.
protocol TimelineProxyProtocol {
protocol TimelineProxyProtocol: Sendable {
var timelineItemProvider: TimelineItemProviderProtocol { get }
func subscribeForUpdates() async