Move the pinned timeline in line with the other timeline creation methods and have it report errors just like the others

This commit is contained in:
Stefan Ceriu
2025-05-21 22:16:03 +03:00
committed by Stefan Ceriu
parent 2c4a8b34a7
commit d4e505854a
13 changed files with 180 additions and 121 deletions

View File

@@ -67,10 +67,10 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()),
stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID))
guard let timelineController = await timelineControllerFactory.buildPinnedEventsTimelineController(roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) else {
guard case let .success(timelineController) = await timelineControllerFactory.buildPinnedEventsTimelineController(roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider) else {
fatalError("This can never fail because we allow this view to be presented only when the timeline is fully loaded and not nil")
}

View File

@@ -6270,23 +6270,6 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
set(value) { underlyingTimeline = value }
}
var underlyingTimeline: TimelineProxyProtocol!
var pinnedEventsTimelineCallsCount = 0
var pinnedEventsTimelineCalled: Bool {
return pinnedEventsTimelineCallsCount > 0
}
var pinnedEventsTimeline: TimelineProxyProtocol? {
get async {
pinnedEventsTimelineCallsCount += 1
if let pinnedEventsTimelineClosure = pinnedEventsTimelineClosure {
return await pinnedEventsTimelineClosure()
} else {
return underlyingPinnedEventsTimeline
}
}
}
var underlyingPinnedEventsTimeline: TimelineProxyProtocol?
var pinnedEventsTimelineClosure: (() async -> TimelineProxyProtocol?)?
var id: String {
get { return underlyingId }
set(value) { underlyingId = value }
@@ -6578,6 +6561,70 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
return messageFilteredTimelineFocusAllowedMessageTypesPresentationReturnValue
}
}
//MARK: - pinnedEventsTimeline
var pinnedEventsTimelineUnderlyingCallsCount = 0
var pinnedEventsTimelineCallsCount: Int {
get {
if Thread.isMainThread {
return pinnedEventsTimelineUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = pinnedEventsTimelineUnderlyingCallsCount
}
return returnValue!
}
}
set {
if Thread.isMainThread {
pinnedEventsTimelineUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
pinnedEventsTimelineUnderlyingCallsCount = newValue
}
}
}
}
var pinnedEventsTimelineCalled: Bool {
return pinnedEventsTimelineCallsCount > 0
}
var pinnedEventsTimelineUnderlyingReturnValue: Result<TimelineProxyProtocol, RoomProxyError>!
var pinnedEventsTimelineReturnValue: Result<TimelineProxyProtocol, RoomProxyError>! {
get {
if Thread.isMainThread {
return pinnedEventsTimelineUnderlyingReturnValue
} else {
var returnValue: Result<TimelineProxyProtocol, RoomProxyError>? = nil
DispatchQueue.main.sync {
returnValue = pinnedEventsTimelineUnderlyingReturnValue
}
return returnValue!
}
}
set {
if Thread.isMainThread {
pinnedEventsTimelineUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
pinnedEventsTimelineUnderlyingReturnValue = newValue
}
}
}
}
var pinnedEventsTimelineClosure: (() async -> Result<TimelineProxyProtocol, RoomProxyError>)?
func pinnedEventsTimeline() async -> Result<TimelineProxyProtocol, RoomProxyError> {
pinnedEventsTimelineCallsCount += 1
if let pinnedEventsTimelineClosure = pinnedEventsTimelineClosure {
return await pinnedEventsTimelineClosure()
} else {
return pinnedEventsTimelineReturnValue
}
}
//MARK: - enableEncryption
var enableEncryptionUnderlyingCallsCount = 0
@@ -15141,13 +15188,13 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments: (roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)?
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedInvocations: [(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol)] = []
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: TimelineControllerProtocol?
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: TimelineControllerProtocol? {
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>!
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReturnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>! {
get {
if Thread.isMainThread {
return buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
} else {
var returnValue: TimelineControllerProtocol?? = nil
var returnValue: Result<TimelineControllerProtocol, TimelineFactoryControllerError>? = nil
DispatchQueue.main.sync {
returnValue = buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderUnderlyingReturnValue
}
@@ -15165,9 +15212,9 @@ class TimelineControllerFactoryMock: TimelineControllerFactoryProtocol, @uncheck
}
}
}
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> TimelineControllerProtocol?)?
var buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderClosure: ((JoinedRoomProxyProtocol, RoomTimelineItemFactoryProtocol, MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>)?
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? {
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol, timelineItemFactory: RoomTimelineItemFactoryProtocol, mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderCallsCount += 1
buildPinnedEventsTimelineControllerRoomProxyTimelineItemFactoryMediaProviderReceivedArguments = (roomProxy: roomProxy, timelineItemFactory: timelineItemFactory, mediaProvider: mediaProvider)
DispatchQueue.main.async {

View File

@@ -59,6 +59,8 @@ extension JoinedRoomProxyMock {
timeline = TimelineProxyMock(.init(isAutoUpdating: configuration.shouldUseAutoUpdatingTimeline,
timelineStartReached: configuration.timelineStartReached))
pinnedEventsTimelineReturnValue = .failure(.failedCreatingPinnedTimeline)
ownUserID = configuration.ownUserID

View File

@@ -445,12 +445,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
Task {
guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else {
guard case let .success(pinnedEventsTimeline) = await roomProxy.pinnedEventsTimeline() else {
return
}
if pinnedEventsTimelineProvider == nil {
pinnedEventsTimelineProvider = timelineProvider
pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider
}
}
}

View File

@@ -353,12 +353,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
Task {
guard let timelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else {
guard case let .success(pinnedEventsTimeline) = await roomProxy.pinnedEventsTimeline() else {
return
}
if pinnedEventsTimelineProvider == nil {
pinnedEventsTimelineProvider = timelineProvider
pinnedEventsTimelineProvider = pinnedEventsTimeline.timelineProvider
}
}
}

View File

@@ -11,12 +11,14 @@ import MatrixRustSDK
class BannedRoomProxy: BannedRoomProxyProtocol {
private let roomListItem: RoomListItemProtocol
private let roomPreview: RoomPreviewProtocol
let info: BaseRoomInfoProxyProtocol
let ownUserID: String
// A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI
lazy var id = info.id
let ownUserID: String
let info: BaseRoomInfoProxyProtocol
init(roomListItem: RoomListItemProtocol,
roomPreview: RoomPreviewProtocol,

View File

@@ -12,13 +12,15 @@ import UIKit
class InvitedRoomProxy: InvitedRoomProxyProtocol {
private let roomListItem: RoomListItemProtocol
private let roomPreview: RoomPreviewProtocol
let info: BaseRoomInfoProxyProtocol
let ownUserID: String
let inviter: RoomMemberProxyProtocol?
// A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI
lazy var id: String = info.id
let ownUserID: String
let info: BaseRoomInfoProxyProtocol
let inviter: RoomMemberProxyProtocol?
init(roomListItem: RoomListItemProtocol,
roomPreview: RoomPreviewProtocol,

View File

@@ -14,50 +14,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private let roomListService: RoomListServiceProtocol
private let roomListItem: RoomListItemProtocol
private let room: RoomProtocol
let timeline: TimelineProxyProtocol
private var innerPinnedEventsTimeline: TimelineProxyProtocol?
private var innerPinnedEventsTimelineTask: Task<TimelineProxyProtocol?, Never>?
var pinnedEventsTimeline: TimelineProxyProtocol? {
get async {
// Check if is already available.
if let innerPinnedEventsTimeline {
return innerPinnedEventsTimeline
// Otherwise check if there is already a task loading it, and wait for it.
} else if let innerPinnedEventsTimelineTask,
let value = await innerPinnedEventsTimelineTask.value {
return value
// Else create and store a new task to load it and wait for it.
} else {
let task = Task<TimelineProxyProtocol?, Never> { [weak self] in
guard let self else {
return nil
}
do {
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10),
filter: .all,
internalIdPrefix: nil,
dateDividerMode: .daily,
trackReadReceipts: false,
reportUtds: true))
let timeline = TimelineProxy(timeline: sdkTimeline, kind: .pinned)
await timeline.subscribeForUpdates()
innerPinnedEventsTimeline = timeline
return timeline
} catch {
MXLog.error("Failed creating pinned events timeline with error: \(error)")
return nil
}
}
innerPinnedEventsTimelineTask = task
return await task.value
}
}
}
// periphery:ignore - required for instance retention in the rust codebase
private var roomInfoObservationToken: TaskHandle?
@@ -68,8 +24,19 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
// periphery:ignore - required for instance retention in the rust codebase
private var knockRequestsChangesObservationToken: TaskHandle?
private var innerPinnedEventsTimeline: TimelineProxyProtocol?
private var innerPinnedEventsTimelineTask: Task<Result<TimelineProxyProtocol, RoomProxyError>, Never>?
private var subscribedForUpdates = false
// A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI
lazy var id: String = room.id()
var ownUserID: String { room.ownUserId() }
let timeline: TimelineProxyProtocol
private let infoSubject: CurrentValueSubject<RoomInfoProxy, Never>
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
infoSubject.asCurrentValuePublisher()
@@ -95,12 +62,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
knockRequestsStateSubject.asCurrentValuePublisher()
}
// A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI
lazy var id: String = room.id()
var ownUserID: String { room.ownUserId() }
var info: RoomInfoProxy { infoSubject.value }
init(roomListService: RoomListServiceProtocol,
roomListItem: RoomListItemProtocol,
room: RoomProtocol) async throws {
@@ -111,7 +72,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo()))
timeline = try await TimelineProxy(timeline: room.timelineWithConfiguration(configuration: .init(focus: .live,
filter: .eventTypeFilter(filter: eventFilters),
filter: .eventTypeFilter(filter: excludedEventsFilter),
internalIdPrefix: nil,
dateDividerMode: .daily,
trackReadReceipts: true,
@@ -131,24 +92,6 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
}
}
private let eventFilters: TimelineEventTypeFilter = {
var stateEventFilters: [StateEventType] = [.roomAliases,
.roomCanonicalAlias,
.roomGuestAccess,
.roomHistoryVisibility,
.roomJoinRules,
.roomPinnedEvents,
.roomPowerLevels,
.roomServerAcl,
.roomTombstone,
.spaceChild,
.spaceParent,
.policyRuleRoom,
.policyRuleServer,
.policyRuleUser]
return .exclude(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) })
}()
func subscribeForUpdates() async {
guard !subscribedForUpdates else {
MXLog.warning("Room already subscribed for updates")
@@ -273,6 +216,43 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
}
}
func pinnedEventsTimeline() async -> Result<TimelineProxyProtocol, RoomProxyError> {
// Check if is already available.
if let innerPinnedEventsTimeline {
return .success(innerPinnedEventsTimeline)
// Otherwise check if there is already a task loading it, and wait for it.
} else if let innerPinnedEventsTimelineTask {
return await innerPinnedEventsTimelineTask.value
} else { // Else create and store a new task to load it and wait for it.
let task = Task<Result<TimelineProxyProtocol, RoomProxyError>, Never> { [weak self] in
guard let self else {
return .failure(.failedCreatingPinnedTimeline)
}
do {
let sdkTimeline = try await room.timelineWithConfiguration(configuration: .init(focus: .pinnedEvents(maxEventsToLoad: 100, maxConcurrentRequests: 10),
filter: .all,
internalIdPrefix: nil,
dateDividerMode: .daily,
trackReadReceipts: false,
reportUtds: true))
let timeline = TimelineProxy(timeline: sdkTimeline, kind: .pinned)
await timeline.subscribeForUpdates()
innerPinnedEventsTimeline = timeline
return .success(timeline)
} catch {
MXLog.error("Failed creating pinned events timeline with error: \(error)")
return .failure(.sdkError(error))
}
}
innerPinnedEventsTimelineTask = task
return await task.value
}
}
func enableEncryption() async -> Result<Void, RoomProxyError> {
do {
try await room.enableEncryption()
@@ -856,6 +836,24 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
MXLog.error("Failed observing requests to join with error: \(error)")
}
}
private let excludedEventsFilter: TimelineEventTypeFilter = {
var stateEventFilters: [StateEventType] = [.roomAliases,
.roomCanonicalAlias,
.roomGuestAccess,
.roomHistoryVisibility,
.roomJoinRules,
.roomPinnedEvents,
.roomPowerLevels,
.roomServerAcl,
.roomTombstone,
.spaceChild,
.spaceParent,
.policyRuleRoom,
.policyRuleServer,
.policyRuleUser]
return .exclude(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) })
}()
}
private final class RoomTypingNotificationUpdateListener: TypingNotificationsListener {

View File

@@ -11,12 +11,14 @@ import MatrixRustSDK
class KnockedRoomProxy: KnockedRoomProxyProtocol {
private let roomListItem: RoomListItemProtocol
private let roomPreview: RoomPreviewProtocol
let info: BaseRoomInfoProxyProtocol
let ownUserID: String
// A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI
lazy var id = info.id
let ownUserID: String
let info: BaseRoomInfoProxyProtocol
init(roomListItem: RoomListItemProtocol,
roomPreview: RoomPreviewProtocol,

View File

@@ -16,6 +16,7 @@ enum RoomProxyError: Error {
case invalidMedia
case eventNotFound
case missingTransactionID
case failedCreatingPinnedTimeline
}
/// An enum that describes the relationship between the current user and the room, and contains a reference to the specific implementation of the `RoomProxy`.
@@ -75,8 +76,6 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var timeline: TimelineProxyProtocol { get }
var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
func subscribeForUpdates() async
func subscribeToRoomInfoUpdates()
@@ -89,6 +88,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
allowedMessageTypes: [TimelineAllowedMessageType],
presentation: TimelineKind.MediaPresentation) async -> Result<TimelineProxyProtocol, RoomProxyError>
func pinnedEventsTimeline() async -> Result<TimelineProxyProtocol, RoomProxyError>
func enableEncryption() async -> Result<Void, RoomProxyError>
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>

View File

@@ -40,17 +40,18 @@ struct TimelineControllerFactory: TimelineControllerFactoryProtocol {
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol? {
guard let pinnedEventsTimeline = await roomProxy.pinnedEventsTimeline else {
return nil
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError> {
switch await roomProxy.pinnedEventsTimeline() {
case .success(let timelineProxy):
return .success(TimelineController(roomProxy: roomProxy,
timelineProxy: timelineProxy,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings))
case .failure(let error):
return .failure(.roomProxyError(error))
}
return TimelineController(roomProxy: roomProxy,
timelineProxy: pinnedEventsTimeline,
initialFocussedEventID: nil,
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider,
appSettings: ServiceLocator.shared.settings)
}
func buildMessageFilteredTimelineController(focus: TimelineFocus,

View File

@@ -26,7 +26,7 @@ protocol TimelineControllerFactoryProtocol {
func buildPinnedEventsTimelineController(roomProxy: JoinedRoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) async -> TimelineControllerProtocol?
mediaProvider: MediaProviderProtocol) async -> Result<TimelineControllerProtocol, TimelineFactoryControllerError>
func buildMessageFilteredTimelineController(focus: TimelineFocus,
allowedMessageTypes: [TimelineAllowedMessageType],

View File

@@ -30,7 +30,11 @@ class RoomScreenViewModelTests: XCTestCase {
let roomProxyMock = JoinedRoomProxyMock(configuration)
// setup a way to inject the mock of the pinned events timeline
roomProxyMock.pinnedEventsTimelineClosure = {
await timelineSubject.values.first()
guard let timeline = await timelineSubject.values.first() else {
fatalError()
}
return .success(timeline)
}
// setup the room proxy actions publisher
roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
@@ -113,7 +117,7 @@ class RoomScreenViewModelTests: XCTestCase {
pinnedTimelineProviderMock.itemProxies = [.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test1")), uniqueID: .init("1"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test2")), uniqueID: .init("2"))),
.event(.init(item: EventTimelineItem(configuration: .init(eventID: "test3")), uniqueID: .init("3")))]
roomProxyMock.underlyingPinnedEventsTimeline = pinnedTimelineMock
roomProxyMock.pinnedEventsTimelineReturnValue = .success(pinnedTimelineMock)
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock,
initialSelectedPinnedEventID: "test1",