diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index d1fe29f32..f3619fbf2 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ 2F623DA1122140A987B34D08 /* NotificationSettingsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */; }; 2F66701B15657A87B4AC3A0A /* WaitlistScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CE2B7AD979BDEE09FEDB08 /* WaitlistScreenModels.swift */; }; 2F94054F50E312AF30BE07F3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40B21E611DADDEF00307E7AC /* String.swift */; }; + 2FEC6652055984389CE1BBEC /* TimelineProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */; }; 3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */; }; 308BD9343B95657FAA583FB7 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; }; 3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1FFC3336EB23374BBBFCC /* UIKitBackgroundTaskService.swift */; }; @@ -889,6 +890,7 @@ E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */; }; E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; }; E79D79CDAFE8BEBCC3AECA54 /* AppLockScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */; }; + E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E543072DE58E751F028998 /* TimelineProxy.swift */; }; E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; }; E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; }; @@ -1658,6 +1660,7 @@ B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = ""; }; B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = ""; }; B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; + B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyProtocol.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; @@ -1912,6 +1915,7 @@ F899D02CF26EA7675EEBE74C /* UserSessionScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionScreenTests.swift; sourceTree = ""; }; F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreen.swift; sourceTree = ""; }; F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenUITests.swift; sourceTree = ""; }; + F9E543072DE58E751F028998 /* TimelineProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxy.swift; sourceTree = ""; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; F9ED8E731E21055F728E5FED /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = ""; }; FA7BB497B2F539C17E88F6B7 /* NotificationSettingsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -4653,6 +4657,8 @@ 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */, 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */, 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */, + F9E543072DE58E751F028998 /* TimelineProxy.swift */, + B50F03079F6B5EF9CA005F14 /* TimelineProxyProtocol.swift */, 3EA31CC7012EA2A5653DAFC9 /* Fixtures */, 2F2FED77226A43559F009463 /* TimelineController */, 6B0910BCE4F1B02F124E1A09 /* TimelineItemContent */, @@ -5869,6 +5875,8 @@ 1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */, A680F54935A6ADEA4ED6C38F /* TimelineItemStatusView.swift in Sources */, 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */, + E82E13CC3EB923CCB8F8273C /* TimelineProxy.swift in Sources */, + 2FEC6652055984389CE1BBEC /* TimelineProxyProtocol.swift in Sources */, 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */, 49814A48470F347426513B07 /* TimelineReadReceiptsView.swift in Sources */, 2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */, @@ -6652,7 +6660,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = "0.0.6-november23"; + version = "0.0.7-november23"; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4500f3f07..893e8d900 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -130,8 +130,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "42274cc2414e675b246432b037e7fa82b587fd97", - "version" : "0.0.6-november23" + "revision" : "aa1dd4fc587d4b4adf603fd7ffef1580c9955d0c", + "version" : "0.0.7-november23" } }, { diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index ff21cbc30..45bee165e 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -266,9 +266,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, return } let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) - switch await roomProxy?.sendMessage(replyText, - html: nil, - intentionalMentions: .empty) { + switch await roomProxy?.timeline.sendMessage(replyText, + html: nil, + intentionalMentions: .empty) { case .success: break default: diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index e64d5738e..d771a5deb 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -595,20 +595,20 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private func presentMapNavigator(interactionMode: StaticLocationInteractionMode) { let locationPickerNavigationStackCoordinator = NavigationStackCoordinator() - + let params = StaticLocationScreenCoordinatorParameters(interactionMode: interactionMode) let coordinator = StaticLocationScreenCoordinator(parameters: params) - + coordinator.actions.sink { [weak self] action in guard let self else { return } switch action { case .selectedLocation(let geoURI, let isUserLocation): Task { - _ = await self.roomProxy?.sendLocation(body: geoURI.bodyMessage, - geoURI: geoURI, - description: nil, - zoomLevel: 15, - assetType: isUserLocation ? .sender : .pin) + _ = await self.roomProxy?.timeline.sendLocation(body: geoURI.bodyMessage, + geoURI: geoURI, + description: nil, + zoomLevel: 15, + assetType: isUserLocation ? .sender : .pin) self.navigationSplitCoordinator.setSheetCoordinator(nil) } @@ -622,9 +622,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } .store(in: &cancellables) - + locationPickerNavigationStackCoordinator.setRootCoordinator(coordinator) - + navigationStackCoordinator.setSheetCoordinator(locationPickerNavigationStackCoordinator) { [weak self] in self?.stateMachine.tryEvent(.dismissMapNavigator) } @@ -671,7 +671,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return } - let result = await roomProxy.createPoll(question: question, answers: options, pollKind: pollKind) + let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind) self.analytics.trackComposer(inThread: false, isEditing: false, @@ -697,7 +697,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return } - let result = await roomProxy.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind) + let result = await roomProxy.timeline.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind) switch result { case .success: @@ -781,7 +781,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return } - guard let messageEventContent = roomProxy.messageEventContent(for: eventID) else { + guard let messageEventContent = roomProxy.timeline.messageEventContent(for: eventID) else { MXLog.error("Failed retrieving forwarded message event content for eventID: \(eventID)") userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) return @@ -793,7 +793,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return } - if case .failure(let error) = await targetRoomProxy.sendMessageEventContent(messageEventContent) { + if case .failure(let error) = await targetRoomProxy.timeline.sendMessageEventContent(messageEventContent) { MXLog.error("Failed forwarding message with error: \(error)") userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown)) return diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index cb4dcd478..a87109541 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1934,11 +1934,16 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingStateUpdatesPublisher = value } } var underlyingStateUpdatesPublisher: AnyPublisher! - var timelineProvider: RoomTimelineProviderProtocol { - get { return underlyingTimelineProvider } - set(value) { underlyingTimelineProvider = value } + var timeline: TimelineProxyProtocol { + get { return underlyingTimeline } + set(value) { underlyingTimeline = value } } - var underlyingTimelineProvider: RoomTimelineProviderProtocol! + var underlyingTimeline: TimelineProxyProtocol! + var pollHistoryTimeline: TimelineProxyProtocol { + get { return underlyingPollHistoryTimeline } + set(value) { underlyingPollHistoryTimeline = value } + } + var underlyingPollHistoryTimeline: TimelineProxyProtocol! //MARK: - subscribeForUpdates @@ -1994,291 +1999,6 @@ class RoomProxyMock: RoomProxyProtocol { return loadDisplayNameForUserIdReturnValue } } - //MARK: - paginateBackwards - - var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount = 0 - var paginateBackwardsRequestSizeUntilNumberOfItemsCalled: Bool { - return paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount > 0 - } - var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments: (requestSize: UInt, untilNumberOfItems: UInt)? - var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations: [(requestSize: UInt, untilNumberOfItems: UInt)] = [] - var paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue: Result! - var paginateBackwardsRequestSizeUntilNumberOfItemsClosure: ((UInt, UInt) async -> Result)? - - func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { - paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount += 1 - paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments = (requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) - paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations.append((requestSize: requestSize, untilNumberOfItems: untilNumberOfItems)) - if let paginateBackwardsRequestSizeUntilNumberOfItemsClosure = paginateBackwardsRequestSizeUntilNumberOfItemsClosure { - return await paginateBackwardsRequestSizeUntilNumberOfItemsClosure(requestSize, untilNumberOfItems) - } else { - return paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue - } - } - //MARK: - sendReadReceipt - - var sendReadReceiptForCallsCount = 0 - var sendReadReceiptForCalled: Bool { - return sendReadReceiptForCallsCount > 0 - } - var sendReadReceiptForReceivedEventID: String? - var sendReadReceiptForReceivedInvocations: [String] = [] - var sendReadReceiptForReturnValue: Result! - var sendReadReceiptForClosure: ((String) async -> Result)? - - func sendReadReceipt(for eventID: String) async -> Result { - sendReadReceiptForCallsCount += 1 - sendReadReceiptForReceivedEventID = eventID - sendReadReceiptForReceivedInvocations.append(eventID) - if let sendReadReceiptForClosure = sendReadReceiptForClosure { - return await sendReadReceiptForClosure(eventID) - } else { - return sendReadReceiptForReturnValue - } - } - //MARK: - messageEventContent - - var messageEventContentForCallsCount = 0 - var messageEventContentForCalled: Bool { - return messageEventContentForCallsCount > 0 - } - var messageEventContentForReceivedEventID: String? - var messageEventContentForReceivedInvocations: [String] = [] - var messageEventContentForReturnValue: RoomMessageEventContentWithoutRelation? - var messageEventContentForClosure: ((String) -> RoomMessageEventContentWithoutRelation?)? - - func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? { - messageEventContentForCallsCount += 1 - messageEventContentForReceivedEventID = eventID - messageEventContentForReceivedInvocations.append(eventID) - if let messageEventContentForClosure = messageEventContentForClosure { - return messageEventContentForClosure(eventID) - } else { - return messageEventContentForReturnValue - } - } - //MARK: - sendMessageEventContent - - var sendMessageEventContentCallsCount = 0 - var sendMessageEventContentCalled: Bool { - return sendMessageEventContentCallsCount > 0 - } - var sendMessageEventContentReceivedMessageContent: RoomMessageEventContentWithoutRelation? - var sendMessageEventContentReceivedInvocations: [RoomMessageEventContentWithoutRelation] = [] - var sendMessageEventContentReturnValue: Result! - var sendMessageEventContentClosure: ((RoomMessageEventContentWithoutRelation) async -> Result)? - - func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result { - sendMessageEventContentCallsCount += 1 - sendMessageEventContentReceivedMessageContent = messageContent - sendMessageEventContentReceivedInvocations.append(messageContent) - if let sendMessageEventContentClosure = sendMessageEventContentClosure { - return await sendMessageEventContentClosure(messageContent) - } else { - return sendMessageEventContentReturnValue - } - } - //MARK: - sendMessage - - var sendMessageHtmlInReplyToIntentionalMentionsCallsCount = 0 - var sendMessageHtmlInReplyToIntentionalMentionsCalled: Bool { - return sendMessageHtmlInReplyToIntentionalMentionsCallsCount > 0 - } - var sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments: (message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)? - var sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations: [(message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)] = [] - var sendMessageHtmlInReplyToIntentionalMentionsReturnValue: Result! - var sendMessageHtmlInReplyToIntentionalMentionsClosure: ((String, String?, String?, IntentionalMentions) async -> Result)? - - func sendMessage(_ message: String, html: String?, inReplyTo eventID: String?, intentionalMentions: IntentionalMentions) async -> Result { - sendMessageHtmlInReplyToIntentionalMentionsCallsCount += 1 - sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments = (message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions) - sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations.append((message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions)) - if let sendMessageHtmlInReplyToIntentionalMentionsClosure = sendMessageHtmlInReplyToIntentionalMentionsClosure { - return await sendMessageHtmlInReplyToIntentionalMentionsClosure(message, html, eventID, intentionalMentions) - } else { - return sendMessageHtmlInReplyToIntentionalMentionsReturnValue - } - } - //MARK: - toggleReaction - - var toggleReactionToCallsCount = 0 - var toggleReactionToCalled: Bool { - return toggleReactionToCallsCount > 0 - } - var toggleReactionToReceivedArguments: (reaction: String, eventID: String)? - var toggleReactionToReceivedInvocations: [(reaction: String, eventID: String)] = [] - var toggleReactionToReturnValue: Result! - var toggleReactionToClosure: ((String, String) async -> Result)? - - func toggleReaction(_ reaction: String, to eventID: String) async -> Result { - toggleReactionToCallsCount += 1 - toggleReactionToReceivedArguments = (reaction: reaction, eventID: eventID) - toggleReactionToReceivedInvocations.append((reaction: reaction, eventID: eventID)) - if let toggleReactionToClosure = toggleReactionToClosure { - return await toggleReactionToClosure(reaction, eventID) - } else { - return toggleReactionToReturnValue - } - } - //MARK: - sendImage - - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount = 0 - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCalled: Bool { - return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount > 0 - } - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleReturnValue: Result! - var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure: ((URL, URL, ImageInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - - func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure = sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure { - return await sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure(url, thumbnailURL, imageInfo, progressSubject, requestHandle) - } else { - return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleReturnValue - } - } - //MARK: - sendVideo - - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount = 0 - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCalled: Bool { - return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount > 0 - } - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleReturnValue: Result! - var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure: ((URL, URL, VideoInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - - func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure { - return await sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure(url, thumbnailURL, videoInfo, progressSubject, requestHandle) - } else { - return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleReturnValue - } - } - //MARK: - sendAudio - - var sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount = 0 - var sendAudioUrlAudioInfoProgressSubjectRequestHandleCalled: Bool { - return sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount > 0 - } - var sendAudioUrlAudioInfoProgressSubjectRequestHandleReturnValue: Result! - var sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure: ((URL, AudioInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - - func sendAudio(url: URL, audioInfo: AudioInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure = sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure { - return await sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure(url, audioInfo, progressSubject, requestHandle) - } else { - return sendAudioUrlAudioInfoProgressSubjectRequestHandleReturnValue - } - } - //MARK: - sendFile - - var sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount = 0 - var sendFileUrlFileInfoProgressSubjectRequestHandleCalled: Bool { - return sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount > 0 - } - var sendFileUrlFileInfoProgressSubjectRequestHandleReturnValue: Result! - var sendFileUrlFileInfoProgressSubjectRequestHandleClosure: ((URL, FileInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - - func sendFile(url: URL, fileInfo: FileInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount += 1 - if let sendFileUrlFileInfoProgressSubjectRequestHandleClosure = sendFileUrlFileInfoProgressSubjectRequestHandleClosure { - return await sendFileUrlFileInfoProgressSubjectRequestHandleClosure(url, fileInfo, progressSubject, requestHandle) - } else { - return sendFileUrlFileInfoProgressSubjectRequestHandleReturnValue - } - } - //MARK: - sendLocation - - var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount = 0 - var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCalled: Bool { - return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount > 0 - } - var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments: (body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)? - var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations: [(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)] = [] - var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue: Result! - var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure: ((String, GeoURI, String?, UInt8?, AssetType?) async -> Result)? - - func sendLocation(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?) async -> Result { - sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount += 1 - sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments = (body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType) - sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations.append((body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType)) - if let sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure { - return await sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure(body, geoURI, description, zoomLevel, assetType) - } else { - return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue - } - } - //MARK: - sendVoiceMessage - - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount = 0 - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled: Bool { - return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount > 0 - } - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue: Result! - var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure: ((URL, AudioInfo, [UInt16], CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? - - func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount += 1 - if let sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure { - return await sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure(url, audioInfo, waveform, progressSubject, requestHandle) - } else { - return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue - } - } - //MARK: - retrySend - - var retrySendTransactionIDCallsCount = 0 - var retrySendTransactionIDCalled: Bool { - return retrySendTransactionIDCallsCount > 0 - } - var retrySendTransactionIDReceivedTransactionID: String? - var retrySendTransactionIDReceivedInvocations: [String] = [] - var retrySendTransactionIDClosure: ((String) async -> Void)? - - func retrySend(transactionID: String) async { - retrySendTransactionIDCallsCount += 1 - retrySendTransactionIDReceivedTransactionID = transactionID - retrySendTransactionIDReceivedInvocations.append(transactionID) - await retrySendTransactionIDClosure?(transactionID) - } - //MARK: - cancelSend - - var cancelSendTransactionIDCallsCount = 0 - var cancelSendTransactionIDCalled: Bool { - return cancelSendTransactionIDCallsCount > 0 - } - var cancelSendTransactionIDReceivedTransactionID: String? - var cancelSendTransactionIDReceivedInvocations: [String] = [] - var cancelSendTransactionIDClosure: ((String) async -> Void)? - - func cancelSend(transactionID: String) async { - cancelSendTransactionIDCallsCount += 1 - cancelSendTransactionIDReceivedTransactionID = transactionID - cancelSendTransactionIDReceivedInvocations.append(transactionID) - await cancelSendTransactionIDClosure?(transactionID) - } - //MARK: - editMessage - - var editMessageHtmlOriginalIntentionalMentionsCallsCount = 0 - var editMessageHtmlOriginalIntentionalMentionsCalled: Bool { - return editMessageHtmlOriginalIntentionalMentionsCallsCount > 0 - } - var editMessageHtmlOriginalIntentionalMentionsReceivedArguments: (newMessage: String, html: String?, eventID: String, intentionalMentions: IntentionalMentions)? - var editMessageHtmlOriginalIntentionalMentionsReceivedInvocations: [(newMessage: String, html: String?, eventID: String, intentionalMentions: IntentionalMentions)] = [] - var editMessageHtmlOriginalIntentionalMentionsReturnValue: Result! - var editMessageHtmlOriginalIntentionalMentionsClosure: ((String, String?, String, IntentionalMentions) async -> Result)? - - func editMessage(_ newMessage: String, html: String?, original eventID: String, intentionalMentions: IntentionalMentions) async -> Result { - editMessageHtmlOriginalIntentionalMentionsCallsCount += 1 - editMessageHtmlOriginalIntentionalMentionsReceivedArguments = (newMessage: newMessage, html: html, eventID: eventID, intentionalMentions: intentionalMentions) - editMessageHtmlOriginalIntentionalMentionsReceivedInvocations.append((newMessage: newMessage, html: html, eventID: eventID, intentionalMentions: intentionalMentions)) - if let editMessageHtmlOriginalIntentionalMentionsClosure = editMessageHtmlOriginalIntentionalMentionsClosure { - return await editMessageHtmlOriginalIntentionalMentionsClosure(newMessage, html, eventID, intentionalMentions) - } else { - return editMessageHtmlOriginalIntentionalMentionsReturnValue - } - } //MARK: - redact var redactCallsCount = 0 @@ -2342,22 +2062,6 @@ class RoomProxyMock: RoomProxyProtocol { return ignoreUserReturnValue } } - //MARK: - retryDecryption - - var retryDecryptionForCallsCount = 0 - var retryDecryptionForCalled: Bool { - return retryDecryptionForCallsCount > 0 - } - var retryDecryptionForReceivedSessionID: String? - var retryDecryptionForReceivedInvocations: [String] = [] - var retryDecryptionForClosure: ((String) async -> Void)? - - func retryDecryption(for sessionID: String) async { - retryDecryptionForCallsCount += 1 - retryDecryptionForReceivedSessionID = sessionID - retryDecryptionForReceivedInvocations.append(sessionID) - await retryDecryptionForClosure?(sessionID) - } //MARK: - leaveRoom var leaveRoomCallsCount = 0 @@ -2459,22 +2163,6 @@ class RoomProxyMock: RoomProxyProtocol { return acceptInvitationReturnValue } } - //MARK: - fetchDetails - - var fetchDetailsForCallsCount = 0 - var fetchDetailsForCalled: Bool { - return fetchDetailsForCallsCount > 0 - } - var fetchDetailsForReceivedEventID: String? - var fetchDetailsForReceivedInvocations: [String] = [] - var fetchDetailsForClosure: ((String) -> Void)? - - func fetchDetails(for eventID: String) { - fetchDetailsForCallsCount += 1 - fetchDetailsForReceivedEventID = eventID - fetchDetailsForReceivedInvocations.append(eventID) - fetchDetailsForClosure?(eventID) - } //MARK: - invite var inviteUserIDCallsCount = 0 @@ -2618,90 +2306,6 @@ class RoomProxyMock: RoomProxyProtocol { return canUserTriggerRoomNotificationUserIDReturnValue } } - //MARK: - createPoll - - var createPollQuestionAnswersPollKindCallsCount = 0 - var createPollQuestionAnswersPollKindCalled: Bool { - return createPollQuestionAnswersPollKindCallsCount > 0 - } - var createPollQuestionAnswersPollKindReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? - var createPollQuestionAnswersPollKindReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] = [] - var createPollQuestionAnswersPollKindReturnValue: Result! - var createPollQuestionAnswersPollKindClosure: ((String, [String], Poll.Kind) async -> Result)? - - func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - createPollQuestionAnswersPollKindCallsCount += 1 - createPollQuestionAnswersPollKindReceivedArguments = (question: question, answers: answers, pollKind: pollKind) - createPollQuestionAnswersPollKindReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind)) - if let createPollQuestionAnswersPollKindClosure = createPollQuestionAnswersPollKindClosure { - return await createPollQuestionAnswersPollKindClosure(question, answers, pollKind) - } else { - return createPollQuestionAnswersPollKindReturnValue - } - } - //MARK: - editPoll - - var editPollOriginalQuestionAnswersPollKindCallsCount = 0 - var editPollOriginalQuestionAnswersPollKindCalled: Bool { - return editPollOriginalQuestionAnswersPollKindCallsCount > 0 - } - var editPollOriginalQuestionAnswersPollKindReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? - var editPollOriginalQuestionAnswersPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] = [] - var editPollOriginalQuestionAnswersPollKindReturnValue: Result! - var editPollOriginalQuestionAnswersPollKindClosure: ((String, String, [String], Poll.Kind) async -> Result)? - - func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - editPollOriginalQuestionAnswersPollKindCallsCount += 1 - editPollOriginalQuestionAnswersPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, pollKind: pollKind) - editPollOriginalQuestionAnswersPollKindReceivedInvocations.append((eventID: eventID, question: question, answers: answers, pollKind: pollKind)) - if let editPollOriginalQuestionAnswersPollKindClosure = editPollOriginalQuestionAnswersPollKindClosure { - return await editPollOriginalQuestionAnswersPollKindClosure(eventID, question, answers, pollKind) - } else { - return editPollOriginalQuestionAnswersPollKindReturnValue - } - } - //MARK: - sendPollResponse - - var sendPollResponsePollStartIDAnswersCallsCount = 0 - var sendPollResponsePollStartIDAnswersCalled: Bool { - return sendPollResponsePollStartIDAnswersCallsCount > 0 - } - var sendPollResponsePollStartIDAnswersReceivedArguments: (pollStartID: String, answers: [String])? - var sendPollResponsePollStartIDAnswersReceivedInvocations: [(pollStartID: String, answers: [String])] = [] - var sendPollResponsePollStartIDAnswersReturnValue: Result! - var sendPollResponsePollStartIDAnswersClosure: ((String, [String]) async -> Result)? - - func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { - sendPollResponsePollStartIDAnswersCallsCount += 1 - sendPollResponsePollStartIDAnswersReceivedArguments = (pollStartID: pollStartID, answers: answers) - sendPollResponsePollStartIDAnswersReceivedInvocations.append((pollStartID: pollStartID, answers: answers)) - if let sendPollResponsePollStartIDAnswersClosure = sendPollResponsePollStartIDAnswersClosure { - return await sendPollResponsePollStartIDAnswersClosure(pollStartID, answers) - } else { - return sendPollResponsePollStartIDAnswersReturnValue - } - } - //MARK: - endPoll - - var endPollPollStartIDTextCallsCount = 0 - var endPollPollStartIDTextCalled: Bool { - return endPollPollStartIDTextCallsCount > 0 - } - var endPollPollStartIDTextReceivedArguments: (pollStartID: String, text: String)? - var endPollPollStartIDTextReceivedInvocations: [(pollStartID: String, text: String)] = [] - var endPollPollStartIDTextReturnValue: Result! - var endPollPollStartIDTextClosure: ((String, String) async -> Result)? - - func endPoll(pollStartID: String, text: String) async -> Result { - endPollPollStartIDTextCallsCount += 1 - endPollPollStartIDTextReceivedArguments = (pollStartID: pollStartID, text: text) - endPollPollStartIDTextReceivedInvocations.append((pollStartID: pollStartID, text: text)) - if let endPollPollStartIDTextClosure = endPollPollStartIDTextClosure { - return await endPollPollStartIDTextClosure(pollStartID, text) - } else { - return endPollPollStartIDTextReturnValue - } - } //MARK: - elementCallWidgetDriver var elementCallWidgetDriverCallsCount = 0 @@ -2963,6 +2567,427 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy } } } +class TimelineProxyMock: TimelineProxyProtocol { + var timelineProvider: RoomTimelineProviderProtocol { + get { return underlyingTimelineProvider } + set(value) { underlyingTimelineProvider = value } + } + var underlyingTimelineProvider: RoomTimelineProviderProtocol! + + //MARK: - subscribeForUpdates + + var subscribeForUpdatesCallsCount = 0 + var subscribeForUpdatesCalled: Bool { + return subscribeForUpdatesCallsCount > 0 + } + var subscribeForUpdatesClosure: (() async -> Void)? + + func subscribeForUpdates() async { + subscribeForUpdatesCallsCount += 1 + await subscribeForUpdatesClosure?() + } + //MARK: - cancelSend + + var cancelSendTransactionIDCallsCount = 0 + var cancelSendTransactionIDCalled: Bool { + return cancelSendTransactionIDCallsCount > 0 + } + var cancelSendTransactionIDReceivedTransactionID: String? + var cancelSendTransactionIDReceivedInvocations: [String] = [] + var cancelSendTransactionIDClosure: ((String) async -> Void)? + + func cancelSend(transactionID: String) async { + cancelSendTransactionIDCallsCount += 1 + cancelSendTransactionIDReceivedTransactionID = transactionID + cancelSendTransactionIDReceivedInvocations.append(transactionID) + await cancelSendTransactionIDClosure?(transactionID) + } + //MARK: - editMessage + + var editMessageHtmlOriginalIntentionalMentionsCallsCount = 0 + var editMessageHtmlOriginalIntentionalMentionsCalled: Bool { + return editMessageHtmlOriginalIntentionalMentionsCallsCount > 0 + } + var editMessageHtmlOriginalIntentionalMentionsReceivedArguments: (message: String, html: String?, eventID: String, intentionalMentions: IntentionalMentions)? + var editMessageHtmlOriginalIntentionalMentionsReceivedInvocations: [(message: String, html: String?, eventID: String, intentionalMentions: IntentionalMentions)] = [] + var editMessageHtmlOriginalIntentionalMentionsReturnValue: Result! + var editMessageHtmlOriginalIntentionalMentionsClosure: ((String, String?, String, IntentionalMentions) async -> Result)? + + func editMessage(_ message: String, html: String?, original eventID: String, intentionalMentions: IntentionalMentions) async -> Result { + editMessageHtmlOriginalIntentionalMentionsCallsCount += 1 + editMessageHtmlOriginalIntentionalMentionsReceivedArguments = (message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions) + editMessageHtmlOriginalIntentionalMentionsReceivedInvocations.append((message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions)) + if let editMessageHtmlOriginalIntentionalMentionsClosure = editMessageHtmlOriginalIntentionalMentionsClosure { + return await editMessageHtmlOriginalIntentionalMentionsClosure(message, html, eventID, intentionalMentions) + } else { + return editMessageHtmlOriginalIntentionalMentionsReturnValue + } + } + //MARK: - fetchDetails + + var fetchDetailsForCallsCount = 0 + var fetchDetailsForCalled: Bool { + return fetchDetailsForCallsCount > 0 + } + var fetchDetailsForReceivedEventID: String? + var fetchDetailsForReceivedInvocations: [String] = [] + var fetchDetailsForClosure: ((String) -> Void)? + + func fetchDetails(for eventID: String) { + fetchDetailsForCallsCount += 1 + fetchDetailsForReceivedEventID = eventID + fetchDetailsForReceivedInvocations.append(eventID) + fetchDetailsForClosure?(eventID) + } + //MARK: - messageEventContent + + var messageEventContentForCallsCount = 0 + var messageEventContentForCalled: Bool { + return messageEventContentForCallsCount > 0 + } + var messageEventContentForReceivedEventID: String? + var messageEventContentForReceivedInvocations: [String] = [] + var messageEventContentForReturnValue: RoomMessageEventContentWithoutRelation? + var messageEventContentForClosure: ((String) -> RoomMessageEventContentWithoutRelation?)? + + func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? { + messageEventContentForCallsCount += 1 + messageEventContentForReceivedEventID = eventID + messageEventContentForReceivedInvocations.append(eventID) + if let messageEventContentForClosure = messageEventContentForClosure { + return messageEventContentForClosure(eventID) + } else { + return messageEventContentForReturnValue + } + } + //MARK: - retryDecryption + + var retryDecryptionForCallsCount = 0 + var retryDecryptionForCalled: Bool { + return retryDecryptionForCallsCount > 0 + } + var retryDecryptionForReceivedSessionID: String? + var retryDecryptionForReceivedInvocations: [String] = [] + var retryDecryptionForClosure: ((String) async -> Void)? + + func retryDecryption(for sessionID: String) async { + retryDecryptionForCallsCount += 1 + retryDecryptionForReceivedSessionID = sessionID + retryDecryptionForReceivedInvocations.append(sessionID) + await retryDecryptionForClosure?(sessionID) + } + //MARK: - retrySend + + var retrySendTransactionIDCallsCount = 0 + var retrySendTransactionIDCalled: Bool { + return retrySendTransactionIDCallsCount > 0 + } + var retrySendTransactionIDReceivedTransactionID: String? + var retrySendTransactionIDReceivedInvocations: [String] = [] + var retrySendTransactionIDClosure: ((String) async -> Void)? + + func retrySend(transactionID: String) async { + retrySendTransactionIDCallsCount += 1 + retrySendTransactionIDReceivedTransactionID = transactionID + retrySendTransactionIDReceivedInvocations.append(transactionID) + await retrySendTransactionIDClosure?(transactionID) + } + //MARK: - paginateBackwards + + var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount = 0 + var paginateBackwardsRequestSizeUntilNumberOfItemsCalled: Bool { + return paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount > 0 + } + var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments: (requestSize: UInt, untilNumberOfItems: UInt)? + var paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations: [(requestSize: UInt, untilNumberOfItems: UInt)] = [] + var paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue: Result! + var paginateBackwardsRequestSizeUntilNumberOfItemsClosure: ((UInt, UInt) async -> Result)? + + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { + paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount += 1 + paginateBackwardsRequestSizeUntilNumberOfItemsReceivedArguments = (requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) + paginateBackwardsRequestSizeUntilNumberOfItemsReceivedInvocations.append((requestSize: requestSize, untilNumberOfItems: untilNumberOfItems)) + if let paginateBackwardsRequestSizeUntilNumberOfItemsClosure = paginateBackwardsRequestSizeUntilNumberOfItemsClosure { + return await paginateBackwardsRequestSizeUntilNumberOfItemsClosure(requestSize, untilNumberOfItems) + } else { + return paginateBackwardsRequestSizeUntilNumberOfItemsReturnValue + } + } + //MARK: - sendAudio + + var sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount = 0 + var sendAudioUrlAudioInfoProgressSubjectRequestHandleCalled: Bool { + return sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount > 0 + } + var sendAudioUrlAudioInfoProgressSubjectRequestHandleReturnValue: Result! + var sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure: ((URL, AudioInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + + func sendAudio(url: URL, audioInfo: AudioInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendAudioUrlAudioInfoProgressSubjectRequestHandleCallsCount += 1 + if let sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure = sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure { + return await sendAudioUrlAudioInfoProgressSubjectRequestHandleClosure(url, audioInfo, progressSubject, requestHandle) + } else { + return sendAudioUrlAudioInfoProgressSubjectRequestHandleReturnValue + } + } + //MARK: - sendFile + + var sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount = 0 + var sendFileUrlFileInfoProgressSubjectRequestHandleCalled: Bool { + return sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount > 0 + } + var sendFileUrlFileInfoProgressSubjectRequestHandleReturnValue: Result! + var sendFileUrlFileInfoProgressSubjectRequestHandleClosure: ((URL, FileInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + + func sendFile(url: URL, fileInfo: FileInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendFileUrlFileInfoProgressSubjectRequestHandleCallsCount += 1 + if let sendFileUrlFileInfoProgressSubjectRequestHandleClosure = sendFileUrlFileInfoProgressSubjectRequestHandleClosure { + return await sendFileUrlFileInfoProgressSubjectRequestHandleClosure(url, fileInfo, progressSubject, requestHandle) + } else { + return sendFileUrlFileInfoProgressSubjectRequestHandleReturnValue + } + } + //MARK: - sendImage + + var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount = 0 + var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCalled: Bool { + return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount > 0 + } + var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleReturnValue: Result! + var sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure: ((URL, URL, ImageInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + + func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleCallsCount += 1 + if let sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure = sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure { + return await sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleClosure(url, thumbnailURL, imageInfo, progressSubject, requestHandle) + } else { + return sendImageUrlThumbnailURLImageInfoProgressSubjectRequestHandleReturnValue + } + } + //MARK: - sendLocation + + var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount = 0 + var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCalled: Bool { + return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount > 0 + } + var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments: (body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)? + var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations: [(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?)] = [] + var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue: Result! + var sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure: ((String, GeoURI, String?, UInt8?, AssetType?) async -> Result)? + + func sendLocation(body: String, geoURI: GeoURI, description: String?, zoomLevel: UInt8?, assetType: AssetType?) async -> Result { + sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeCallsCount += 1 + sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedArguments = (body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType) + sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReceivedInvocations.append((body: body, geoURI: geoURI, description: description, zoomLevel: zoomLevel, assetType: assetType)) + if let sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure = sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure { + return await sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeClosure(body, geoURI, description, zoomLevel, assetType) + } else { + return sendLocationBodyGeoURIDescriptionZoomLevelAssetTypeReturnValue + } + } + //MARK: - sendVideo + + var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount = 0 + var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCalled: Bool { + return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount > 0 + } + var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleReturnValue: Result! + var sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure: ((URL, URL, VideoInfo, CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + + func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo, progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleCallsCount += 1 + if let sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure = sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure { + return await sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleClosure(url, thumbnailURL, videoInfo, progressSubject, requestHandle) + } else { + return sendVideoUrlThumbnailURLVideoInfoProgressSubjectRequestHandleReturnValue + } + } + //MARK: - sendVoiceMessage + + var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount = 0 + var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled: Bool { + return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount > 0 + } + var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue: Result! + var sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure: ((URL, AudioInfo, [UInt16], CurrentValueSubject?, @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result)? + + func sendVoiceMessage(url: URL, audioInfo: AudioInfo, waveform: [UInt16], progressSubject: CurrentValueSubject?, requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCallsCount += 1 + if let sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure { + return await sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure(url, audioInfo, waveform, progressSubject, requestHandle) + } else { + return sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue + } + } + //MARK: - sendReadReceipt + + var sendReadReceiptForCallsCount = 0 + var sendReadReceiptForCalled: Bool { + return sendReadReceiptForCallsCount > 0 + } + var sendReadReceiptForReceivedEventID: String? + var sendReadReceiptForReceivedInvocations: [String] = [] + var sendReadReceiptForReturnValue: Result! + var sendReadReceiptForClosure: ((String) async -> Result)? + + func sendReadReceipt(for eventID: String) async -> Result { + sendReadReceiptForCallsCount += 1 + sendReadReceiptForReceivedEventID = eventID + sendReadReceiptForReceivedInvocations.append(eventID) + if let sendReadReceiptForClosure = sendReadReceiptForClosure { + return await sendReadReceiptForClosure(eventID) + } else { + return sendReadReceiptForReturnValue + } + } + //MARK: - sendMessageEventContent + + var sendMessageEventContentCallsCount = 0 + var sendMessageEventContentCalled: Bool { + return sendMessageEventContentCallsCount > 0 + } + var sendMessageEventContentReceivedMessageContent: RoomMessageEventContentWithoutRelation? + var sendMessageEventContentReceivedInvocations: [RoomMessageEventContentWithoutRelation] = [] + var sendMessageEventContentReturnValue: Result! + var sendMessageEventContentClosure: ((RoomMessageEventContentWithoutRelation) async -> Result)? + + func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result { + sendMessageEventContentCallsCount += 1 + sendMessageEventContentReceivedMessageContent = messageContent + sendMessageEventContentReceivedInvocations.append(messageContent) + if let sendMessageEventContentClosure = sendMessageEventContentClosure { + return await sendMessageEventContentClosure(messageContent) + } else { + return sendMessageEventContentReturnValue + } + } + //MARK: - sendMessage + + var sendMessageHtmlInReplyToIntentionalMentionsCallsCount = 0 + var sendMessageHtmlInReplyToIntentionalMentionsCalled: Bool { + return sendMessageHtmlInReplyToIntentionalMentionsCallsCount > 0 + } + var sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments: (message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)? + var sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations: [(message: String, html: String?, eventID: String?, intentionalMentions: IntentionalMentions)] = [] + var sendMessageHtmlInReplyToIntentionalMentionsReturnValue: Result! + var sendMessageHtmlInReplyToIntentionalMentionsClosure: ((String, String?, String?, IntentionalMentions) async -> Result)? + + func sendMessage(_ message: String, html: String?, inReplyTo eventID: String?, intentionalMentions: IntentionalMentions) async -> Result { + sendMessageHtmlInReplyToIntentionalMentionsCallsCount += 1 + sendMessageHtmlInReplyToIntentionalMentionsReceivedArguments = (message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions) + sendMessageHtmlInReplyToIntentionalMentionsReceivedInvocations.append((message: message, html: html, eventID: eventID, intentionalMentions: intentionalMentions)) + if let sendMessageHtmlInReplyToIntentionalMentionsClosure = sendMessageHtmlInReplyToIntentionalMentionsClosure { + return await sendMessageHtmlInReplyToIntentionalMentionsClosure(message, html, eventID, intentionalMentions) + } else { + return sendMessageHtmlInReplyToIntentionalMentionsReturnValue + } + } + //MARK: - toggleReaction + + var toggleReactionToCallsCount = 0 + var toggleReactionToCalled: Bool { + return toggleReactionToCallsCount > 0 + } + var toggleReactionToReceivedArguments: (reaction: String, eventID: String)? + var toggleReactionToReceivedInvocations: [(reaction: String, eventID: String)] = [] + var toggleReactionToReturnValue: Result! + var toggleReactionToClosure: ((String, String) async -> Result)? + + func toggleReaction(_ reaction: String, to eventID: String) async -> Result { + toggleReactionToCallsCount += 1 + toggleReactionToReceivedArguments = (reaction: reaction, eventID: eventID) + toggleReactionToReceivedInvocations.append((reaction: reaction, eventID: eventID)) + if let toggleReactionToClosure = toggleReactionToClosure { + return await toggleReactionToClosure(reaction, eventID) + } else { + return toggleReactionToReturnValue + } + } + //MARK: - createPoll + + var createPollQuestionAnswersPollKindCallsCount = 0 + var createPollQuestionAnswersPollKindCalled: Bool { + return createPollQuestionAnswersPollKindCallsCount > 0 + } + var createPollQuestionAnswersPollKindReceivedArguments: (question: String, answers: [String], pollKind: Poll.Kind)? + var createPollQuestionAnswersPollKindReceivedInvocations: [(question: String, answers: [String], pollKind: Poll.Kind)] = [] + var createPollQuestionAnswersPollKindReturnValue: Result! + var createPollQuestionAnswersPollKindClosure: ((String, [String], Poll.Kind) async -> Result)? + + func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { + createPollQuestionAnswersPollKindCallsCount += 1 + createPollQuestionAnswersPollKindReceivedArguments = (question: question, answers: answers, pollKind: pollKind) + createPollQuestionAnswersPollKindReceivedInvocations.append((question: question, answers: answers, pollKind: pollKind)) + if let createPollQuestionAnswersPollKindClosure = createPollQuestionAnswersPollKindClosure { + return await createPollQuestionAnswersPollKindClosure(question, answers, pollKind) + } else { + return createPollQuestionAnswersPollKindReturnValue + } + } + //MARK: - editPoll + + var editPollOriginalQuestionAnswersPollKindCallsCount = 0 + var editPollOriginalQuestionAnswersPollKindCalled: Bool { + return editPollOriginalQuestionAnswersPollKindCallsCount > 0 + } + var editPollOriginalQuestionAnswersPollKindReceivedArguments: (eventID: String, question: String, answers: [String], pollKind: Poll.Kind)? + var editPollOriginalQuestionAnswersPollKindReceivedInvocations: [(eventID: String, question: String, answers: [String], pollKind: Poll.Kind)] = [] + var editPollOriginalQuestionAnswersPollKindReturnValue: Result! + var editPollOriginalQuestionAnswersPollKindClosure: ((String, String, [String], Poll.Kind) async -> Result)? + + func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result { + editPollOriginalQuestionAnswersPollKindCallsCount += 1 + editPollOriginalQuestionAnswersPollKindReceivedArguments = (eventID: eventID, question: question, answers: answers, pollKind: pollKind) + editPollOriginalQuestionAnswersPollKindReceivedInvocations.append((eventID: eventID, question: question, answers: answers, pollKind: pollKind)) + if let editPollOriginalQuestionAnswersPollKindClosure = editPollOriginalQuestionAnswersPollKindClosure { + return await editPollOriginalQuestionAnswersPollKindClosure(eventID, question, answers, pollKind) + } else { + return editPollOriginalQuestionAnswersPollKindReturnValue + } + } + //MARK: - endPoll + + var endPollPollStartIDTextCallsCount = 0 + var endPollPollStartIDTextCalled: Bool { + return endPollPollStartIDTextCallsCount > 0 + } + var endPollPollStartIDTextReceivedArguments: (pollStartID: String, text: String)? + var endPollPollStartIDTextReceivedInvocations: [(pollStartID: String, text: String)] = [] + var endPollPollStartIDTextReturnValue: Result! + var endPollPollStartIDTextClosure: ((String, String) async -> Result)? + + func endPoll(pollStartID: String, text: String) async -> Result { + endPollPollStartIDTextCallsCount += 1 + endPollPollStartIDTextReceivedArguments = (pollStartID: pollStartID, text: text) + endPollPollStartIDTextReceivedInvocations.append((pollStartID: pollStartID, text: text)) + if let endPollPollStartIDTextClosure = endPollPollStartIDTextClosure { + return await endPollPollStartIDTextClosure(pollStartID, text) + } else { + return endPollPollStartIDTextReturnValue + } + } + //MARK: - sendPollResponse + + var sendPollResponsePollStartIDAnswersCallsCount = 0 + var sendPollResponsePollStartIDAnswersCalled: Bool { + return sendPollResponsePollStartIDAnswersCallsCount > 0 + } + var sendPollResponsePollStartIDAnswersReceivedArguments: (pollStartID: String, answers: [String])? + var sendPollResponsePollStartIDAnswersReceivedInvocations: [(pollStartID: String, answers: [String])] = [] + var sendPollResponsePollStartIDAnswersReturnValue: Result! + var sendPollResponsePollStartIDAnswersClosure: ((String, [String]) async -> Result)? + + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { + sendPollResponsePollStartIDAnswersCallsCount += 1 + sendPollResponsePollStartIDAnswersReceivedArguments = (pollStartID: pollStartID, answers: answers) + sendPollResponsePollStartIDAnswersReceivedInvocations.append((pollStartID: pollStartID, answers: answers)) + if let sendPollResponsePollStartIDAnswersClosure = sendPollResponsePollStartIDAnswersClosure { + return await sendPollResponsePollStartIDAnswersClosure(pollStartID, answers) + } else { + return sendPollResponsePollStartIDAnswersReturnValue + } + } +} class UserDiscoveryServiceMock: UserDiscoveryServiceProtocol { //MARK: - searchProfiles diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index ec6887003..6b184fcec 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -84,7 +84,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, // MARK: - Private - private func sendAttachment(mediaInfo: MediaInfo, progressSubject: CurrentValueSubject) async -> Result { + private func sendAttachment(mediaInfo: MediaInfo, progressSubject: CurrentValueSubject) async -> Result { let requestHandle: ((SendAttachmentJoinHandleProtocol) -> Void) = { [weak self] handle in self?.requestHandle?.cancel() self?.requestHandle = handle @@ -92,13 +92,13 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, switch mediaInfo { case let .image(imageURL, thumbnailURL, imageInfo): - return await roomProxy.sendImage(url: imageURL, thumbnailURL: thumbnailURL, imageInfo: imageInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendImage(url: imageURL, thumbnailURL: thumbnailURL, imageInfo: imageInfo, progressSubject: progressSubject, requestHandle: requestHandle) case let .video(videoURL, thumbnailURL, videoInfo): - return await roomProxy.sendVideo(url: videoURL, thumbnailURL: thumbnailURL, videoInfo: videoInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendVideo(url: videoURL, thumbnailURL: thumbnailURL, videoInfo: videoInfo, progressSubject: progressSubject, requestHandle: requestHandle) case let .audio(audioURL, audioInfo): - return await roomProxy.sendAudio(url: audioURL, audioInfo: audioInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendAudio(url: audioURL, audioInfo: audioInfo, progressSubject: progressSubject, requestHandle: requestHandle) case let .file(fileURL, fileInfo): - return await roomProxy.sendFile(url: fileURL, fileInfo: fileInfo, progressSubject: progressSubject, requestHandle: requestHandle) + return await roomProxy.timeline.sendFile(url: fileURL, fileInfo: fileInfo, progressSubject: progressSubject, requestHandle: requestHandle) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift index 280ffa9f7..f6b674f52 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift @@ -274,7 +274,7 @@ class RoomScreenInteractionHandler { func sendPollResponse(pollStartID: String, optionID: String) { Task { - let sendPollResponseResult = await roomProxy.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) + let sendPollResponseResult = await roomProxy.timeline.sendPollResponse(pollStartID: pollStartID, answers: [optionID]) analyticsService.trackPollVote() switch sendPollResponseResult { @@ -288,8 +288,8 @@ class RoomScreenInteractionHandler { func endPoll(pollStartID: String) { Task { - let endPollResult = await roomProxy.endPoll(pollStartID: pollStartID, - text: "The poll with event id: \(pollStartID) has ended") + let endPollResult = await roomProxy.timeline.endPoll(pollStartID: pollStartID, + text: "The poll with event id: \(pollStartID) has ended") analyticsService.trackPollEnd() switch endPollResult { case .success: diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index e4f2aa717..a54e6fea7 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -23,48 +23,35 @@ import MatrixRustSDK class RoomProxy: RoomProxyProtocol { private let roomListItem: RoomListItemProtocol private let room: RoomProtocol + let timeline: TimelineProxyProtocol + let pollHistoryTimeline: TimelineProxyProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol private let backgroundTaskName = "SendRoomEvent" - private let messageSendingDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.message_sending", qos: .userInitiated) private let userInitiatedDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.user_initiated", qos: .userInitiated) private let lowPriorityDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.low_priority", qos: .utility) private var sendMessageBackgroundTask: BackgroundTaskProtocol? private(set) var displayName: String? - - private var roomTimelineObservationToken: TaskHandle? - private var backPaginationStateObservationToken: TaskHandle? private var roomInfoObservationToken: TaskHandle? + private var subscribedForUpdates = false - private let backPaginationStateSubject = PassthroughSubject() private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) var members: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { membersSubject.asCurrentValuePublisher() } - - private var timelineListener: RoomTimelineListener? - - private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>() private let stateUpdatesSubject = PassthroughSubject() var stateUpdatesPublisher: AnyPublisher { stateUpdatesSubject.eraseToAnyPublisher() } - - var innerTimelineProvider: RoomTimelineProviderProtocol! - var timelineProvider: RoomTimelineProviderProtocol { - innerTimelineProvider - } var ownUserID: String { room.ownUserId() } deinit { - roomTimelineObservationToken?.cancel() - backPaginationStateObservationToken?.cancel() roomListItem.unsubscribe() } @@ -74,19 +61,25 @@ class RoomProxy: RoomProxyProtocol { self.roomListItem = roomListItem self.room = room self.backgroundTaskService = backgroundTaskService + timeline = await TimelineProxy(timeline: room.timeline(), backgroundTaskService: backgroundTaskService) + pollHistoryTimeline = await TimelineProxy(timeline: room.pollHistory(), backgroundTaskService: backgroundTaskService) Task { - await fetchMembers() + // Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener + // This should become automatic on the RustSDK side at some point + await room.timeline().fetchMembers() + await updateMembers() } } func subscribeForUpdates() async { - guard innerTimelineProvider == nil else { + guard !subscribedForUpdates else { MXLog.warning("Room already subscribed for updates") return } + subscribedForUpdates = true let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""), RequiredState(key: "m.room.topic", value: ""), RequiredState(key: "m.room.avatar", value: ""), @@ -95,26 +88,10 @@ class RoomProxy: RoomProxyProtocol { timelineLimit: UInt32(SlidingSyncConstants.defaultTimelineLimit)) roomListItem.subscribe(settings: settings) - let timelineListener = RoomTimelineListener { [weak self] timelineDiffs in - self?.timelineUpdatesSubject.send(timelineDiffs) - - // Workaround for subscribeToRoomStateUpdates creating problems in the timeline - // https://github.com/matrix-org/matrix-rust-sdk/issues/2488 - self?.stateUpdatesSubject.send() - } + await timeline.subscribeForUpdates() + await pollHistoryTimeline.subscribeForUpdates() - self.timelineListener = timelineListener - - let result = await room.addTimelineListener(listener: timelineListener) - roomTimelineObservationToken = result.itemsStream - - subscribeToBackpagination() - - // subscribeToRoomStateUpdates() - - innerTimelineProvider = await RoomTimelineProvider(currentItems: result.items, - updatePublisher: timelineUpdatesSubject.eraseToAnyPublisher(), - backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher()) + subscribeToRoomStateUpdates() } lazy var id: String = room.id() @@ -216,282 +193,6 @@ class RoomProxy: RoomProxyProtocol { return .failure(.failedRetrievingMemberDisplayName) } } - - func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { - do { - try await Task.dispatch(on: .global()) { - try self.room.paginateBackwards(opts: .untilNumItems(eventLimit: UInt16(requestSize), items: UInt16(untilNumberOfItems), waitForToken: true)) - } - - return .success(()) - } catch { - return .failure(.failedPaginatingBackwards) - } - } - - func sendReadReceipt(for eventID: String) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - return await Task.dispatch(on: lowPriorityDispatchQueue) { - do { - try self.room.sendReadReceipt(eventId: eventID) - return .success(()) - } catch { - return .failure(.failedSendingReadReceipt) - } - } - } - - func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? { - try? room.getTimelineEventContentByEventId(eventId: eventID) - } - - func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - return await Task.dispatch(on: messageSendingDispatchQueue) { - self.room.send(msg: messageContent) - return .success(()) - } - } - - func sendMessage(_ message: String, - html: String?, - inReplyTo eventID: String? = nil, - intentionalMentions: IntentionalMentions) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let messageContent = buildMessageContentFor(message, - html: html, - intentionalMentions: intentionalMentions.toRustMentions()) - - return await Task.dispatch(on: messageSendingDispatchQueue) { - do { - if let eventID { - let replyItem = try self.room.getEventTimelineItemByEventId(eventId: eventID) - try self.room.sendReply(msg: messageContent, replyItem: replyItem) - } else { - self.room.send(msg: messageContent) - } - } catch { - return .failure(.failedSendingMessage) - } - return .success(()) - } - } - - func toggleReaction(_ reaction: String, to eventID: String) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - return await Task.dispatch(on: userInitiatedDispatchQueue) { - do { - try self.room.toggleReaction(eventId: eventID, key: reaction) - return .success(()) - } catch { - return .failure(.failedSendingReaction) - } - } - } - - func sendImage(url: URL, - thumbnailURL: URL, - imageInfo: ImageInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let handle = room.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) - - await requestHandle(handle) - - do { - try await handle.join() - } catch { - return .failure(.failedSendingMedia) - } - - return .success(()) - } - - func sendVideo(url: URL, - thumbnailURL: URL, - videoInfo: VideoInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let handle = room.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) - - await requestHandle(handle) - - do { - try await handle.join() - } catch { - return .failure(.failedSendingMedia) - } - - return .success(()) - } - - func sendAudio(url: URL, - audioInfo: AudioInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let handle = room.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) - - await requestHandle(handle) - - do { - try await handle.join() - } catch { - return .failure(.failedSendingMedia) - } - - return .success(()) - } - - func sendVoiceMessage(url: URL, - audioInfo: AudioInfo, - waveform: [UInt16], - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let handle = room.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) - - await requestHandle(handle) - - do { - try await handle.join() - } catch { - return .failure(.failedSendingMedia) - } - - return .success(()) - } - - func sendFile(url: URL, - fileInfo: FileInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let handle = room.sendFile(url: url.path(percentEncoded: false), fileInfo: fileInfo, progressWatcher: UploadProgressListener { progress in - progressSubject?.send(progress) - }) - - await requestHandle(handle) - - do { - try await handle.join() - } catch { - return .failure(.failedSendingMedia) - } - - return .success(()) - } - - func sendLocation(body: String, - geoURI: GeoURI, - description: String?, - zoomLevel: UInt8?, - assetType: AssetType?) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - return await Task.dispatch(on: messageSendingDispatchQueue) { - .success(self.room.sendLocation(body: body, - geoUri: geoURI.string, - description: description, - zoomLevel: zoomLevel, - assetType: assetType)) - } - } - - func retrySend(transactionID: String) async { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - return await Task.dispatch(on: messageSendingDispatchQueue) { - self.room.retrySend(txnId: transactionID) - } - } - - func cancelSend(transactionID: String) async { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - return await Task.dispatch(on: messageSendingDispatchQueue) { - self.room.cancelSend(txnId: transactionID) - } - } - - func editMessage(_ message: String, - html: String?, - original eventID: String, - intentionalMentions: IntentionalMentions) async -> Result { - sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) - defer { - sendMessageBackgroundTask?.stop() - } - - let messageContent = buildMessageContentFor(message, - html: html, - intentionalMentions: intentionalMentions.toRustMentions()) - - return await Task.dispatch(on: messageSendingDispatchQueue) { - do { - let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID) - try self.room.edit(newContent: messageContent, editItem: originalEvent) - return .success(()) - } catch { - return .failure(.failedEditingMessage) - } - } - } func redact(_ eventID: String) async -> Result { sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) @@ -572,12 +273,6 @@ class RoomProxy: RoomProxyProtocol { } } - func retryDecryption(for sessionID: String) async { - await Task.dispatch(on: .global()) { [weak self] in - self?.room.retryDecryption(sessionIds: [sessionID]) - } - } - func leaveRoom() async -> Result { sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) defer { @@ -626,19 +321,6 @@ class RoomProxy: RoomProxyProtocol { } } - func fetchDetails(for eventID: String) { - Task { - await Task.dispatch(on: .global()) { - do { - MXLog.info("Fetching event details for \(eventID)") - try self.room.fetchDetailsForEvent(eventId: eventID) - } catch { - MXLog.error("Failed fetching event details for \(eventID) with error: \(error)") - } - } - } - } - func invite(userID: String) async -> Result { await Task.dispatch(on: .global()) { do { @@ -713,56 +395,6 @@ class RoomProxy: RoomProxyProtocol { return .failure(.failedCheckingPermission) } } - - // MARK: - Polls - - func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { - await Task.dispatch(on: .global()) { - do { - return try .success(self.room.createPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind))) - } catch { - MXLog.error("Failed creating a poll: \(error)") - return .failure(.failedCreatingPoll) - } - } - } - - func editPoll(original eventID: String, - question: String, - answers: [String], - pollKind: Poll.Kind) async -> Result { - await Task.dispatch(on: .global()) { - do { - let originalEvent = try self.room.getEventTimelineItemByEventId(eventId: eventID) - return try .success(self.room.editPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind), editItem: originalEvent)) - } catch { - MXLog.error("Failed editing the poll: \(error), eventID: \(eventID)") - return .failure(.failedEditingPoll) - } - } - } - - func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { - await Task.dispatch(on: .global()) { - do { - return try .success(self.room.sendPollResponse(pollStartId: pollStartID, answers: answers)) - } catch { - MXLog.error("Failed sending a poll vote: \(error), pollStartID: \(pollStartID)") - return .failure(.failedSendingPollResponse) - } - } - } - - func endPoll(pollStartID: String, text: String) async -> Result { - await Task.dispatch(on: .global()) { - do { - return try .success(self.room.endPoll(pollStartId: pollStartID, text: text)) - } catch { - MXLog.error("Failed ending a poll: \(error), pollStartID: \(pollStartID)") - return .failure(.failedEndingPoll) - } - } - } // MARK: - Element Call @@ -771,64 +403,6 @@ class RoomProxy: RoomProxyProtocol { } // MARK: - Private - - private func buildMessageContentFor(_ message: String, - html: String?, - intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation { - let emoteSlashCommand = "/me " - let isEmote: Bool = message.starts(with: emoteSlashCommand) - - let content: RoomMessageEventContentWithoutRelation - if isEmote { - let emoteMessage = String(message.dropFirst(emoteSlashCommand.count)) - - var emoteHtml: String? - if let html { - emoteHtml = String(html.dropFirst(emoteSlashCommand.count)) - } - content = buildEmoteMessageContentFor(emoteMessage, html: emoteHtml) - } else { - if let html { - content = messageEventContentFromHtml(body: message, htmlBody: html) - } else { - content = messageEventContentFromMarkdown(md: message) - } - } - return content.withMentions(mentions: intentionalMentions) - } - - private func buildEmoteMessageContentFor(_ message: String, html: String?) -> RoomMessageEventContentWithoutRelation { - if let html { - return messageEventContentFromHtmlAsEmote(body: message, htmlBody: html) - } else { - return messageEventContentFromMarkdownAsEmote(md: message) - } - } - - /// Force the timeline to load member details so it can populate sender profiles whenever we add a timeline listener - /// This should become automatic on the RustSDK side at some point - private func fetchMembers() async { - do { - try await room.fetchMembers() - } catch { - MXLog.error("Failed fetching members: \(error)") - } - } - - private func update(displayName: String) { - self.displayName = displayName - } - - private func subscribeToBackpagination() { - let listener = RoomBackpaginationStatusListener { [weak self] status in - self?.backPaginationStateSubject.send(status) - } - do { - backPaginationStateObservationToken = try room.subscribeToBackPaginationStatus(listener: listener) - } catch { - MXLog.error("Failed to subscribe to back pagination state with error: \(error)") - } - } private func subscribeToRoomStateUpdates() { roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in @@ -838,44 +412,6 @@ class RoomProxy: RoomProxyProtocol { } } -private final class RoomTimelineListener: TimelineListener { - private let onUpdateClosure: ([TimelineDiff]) -> Void - - init(_ onUpdateClosure: @escaping ([TimelineDiff]) -> Void) { - self.onUpdateClosure = onUpdateClosure - } - - func onUpdate(diff: [TimelineDiff]) { - onUpdateClosure(diff) - } -} - -private final class UploadProgressListener: ProgressWatcher { - private let onUpdateClosure: (Double) -> Void - - init(_ onUpdateClosure: @escaping (Double) -> Void) { - self.onUpdateClosure = onUpdateClosure - } - - func transmissionProgress(progress: TransmissionProgress) { - DispatchQueue.main.async { [weak self] in - self?.onUpdateClosure(Double(progress.current) / Double(progress.total)) - } - } -} - -private final class RoomBackpaginationStatusListener: BackPaginationStatusListener { - private let onUpdateClosure: (BackPaginationStatus) -> Void - - init(_ onUpdateClosure: @escaping (BackPaginationStatus) -> Void) { - self.onUpdateClosure = onUpdateClosure - } - - func onUpdate(status: BackPaginationStatus) { - onUpdateClosure(status) - } -} - private final class RoomInfoUpdateListener: RoomInfoListener { private let onUpdateClosure: () -> Void @@ -887,14 +423,3 @@ private final class RoomInfoUpdateListener: RoomInfoListener { onUpdateClosure() } } - -private extension MatrixRustSDK.PollKind { - init(pollKind: Poll.Kind) { - switch pollKind { - case .disclosed: - self = .disclosed - case .undisclosed: - self = .undisclosed - } - } -} diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 1de7dcec1..c7f447e6c 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -19,19 +19,10 @@ import Foundation import MatrixRustSDK enum RoomProxyError: Error, Equatable { - case noMoreMessagesToBackPaginate - case failedPaginatingBackwards case failedRetrievingMemberAvatarURL case failedRetrievingMemberDisplayName - case failedSendingReadReceipt - case failedSendingMessage - case failedSendingReaction - case failedSendingMedia - case failedEditingMessage case failedRedactingEvent case failedReportingContent - case failedAddingTimelineListener - case failedRetrievingMembers case failedRetrievingMember case failedLeavingRoom case failedAcceptingInvite @@ -42,10 +33,6 @@ enum RoomProxyError: Error, Equatable { case failedRemovingAvatar case failedUploadingAvatar case failedCheckingPermission - case failedCreatingPoll - case failedSendingPollResponse - case failedEndingPoll - case failedEditingPoll } // sourcery: AutoMockable @@ -82,7 +69,10 @@ protocol RoomProxyProtocol { /// The thread on which this publisher sends the output isn't defined. var stateUpdatesPublisher: AnyPublisher { get } - var timelineProvider: RoomTimelineProviderProtocol { get } + var timeline: TimelineProxyProtocol { get } + + /// A timeline providing just polls related events + var pollHistoryTimeline: TimelineProxyProtocol { get } func subscribeForUpdates() async @@ -90,73 +80,11 @@ protocol RoomProxyProtocol { func loadDisplayNameForUserId(_ userId: String) async -> Result - func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result - - func sendReadReceipt(for eventID: String) async -> Result - - func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? - - func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result - - func sendMessage(_ message: String, - html: String?, - inReplyTo eventID: String?, - intentionalMentions: IntentionalMentions) async -> Result - - func toggleReaction(_ reaction: String, to eventID: String) async -> Result - - func sendImage(url: URL, - thumbnailURL: URL, - imageInfo: ImageInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - - func sendVideo(url: URL, - thumbnailURL: URL, - videoInfo: VideoInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - - func sendAudio(url: URL, - audioInfo: AudioInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - - func sendFile(url: URL, - fileInfo: FileInfo, - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - - func sendLocation(body: String, - geoURI: GeoURI, - description: String?, - zoomLevel: UInt8?, - assetType: AssetType?) async -> Result - - func sendVoiceMessage(url: URL, - audioInfo: AudioInfo, - waveform: [UInt16], - progressSubject: CurrentValueSubject?, - requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result - - /// Retries sending a failed message given its transaction ID - func retrySend(transactionID: String) async - - /// Cancels a failed message given its transaction ID from the timeline - func cancelSend(transactionID: String) async - - func editMessage(_ newMessage: String, - html: String?, - original eventID: String, - intentionalMentions: IntentionalMentions) async -> Result - func redact(_ eventID: String) async -> Result func reportContent(_ eventID: String, reason: String?) async -> Result func ignoreUser(_ userID: String) async -> Result - - func retryDecryption(for sessionID: String) async func leaveRoom() async -> Result @@ -170,8 +98,6 @@ protocol RoomProxyProtocol { func acceptInvitation() async -> Result - func fetchDetails(for eventID: String) - func invite(userID: String) async -> Result func setName(_ name: String) async -> Result @@ -185,14 +111,6 @@ protocol RoomProxyProtocol { func canUserRedact(userID: String) async -> Result func canUserTriggerRoomNotification(userID: String) async -> Result - - func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result - - func editPoll(original eventID: String, question: String, answers: [String], pollKind: Poll.Kind) async -> Result - - func sendPollResponse(pollStartID: String, answers: [String]) async -> Result - - func endPoll(pollStartID: String, text: String) async -> Result // MARK: - Element Call @@ -213,15 +131,6 @@ extension RoomProxyProtocol { } } - func sendMessage(_ message: String, - html: String?, - intentionalMentions: IntentionalMentions) async -> Result { - await sendMessage(message, - html: html, - inReplyTo: nil, - intentionalMentions: intentionalMentions) - } - // Avoids to duplicate the same logic around in the app // Probably this should be done in rust. var roomTitle: String { diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift index 95cbde9c7..ef3c67119 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift @@ -96,22 +96,21 @@ class SecureBackupController: SecureBackupControllerProtocol { return .success(key) } + var keyUploadErrored = false let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in guard let self else { return } switch state { - case .creatingBackup: - recoveryKeyStateSubject.send(.settingUp) - case .creatingRecoveryKey: - recoveryKeyStateSubject.send(.settingUp) - case .backingUp: + case .starting, .creatingBackup, .creatingRecoveryKey, .backingUp: recoveryKeyStateSubject.send(.settingUp) case .done: recoveryKeyStateSubject.send(.enabled) + case .roomKeyUploadError: + keyUploadErrored = true } }) - return .success(recoveryKey) + return keyUploadErrored ? .failure(.failedGeneratingRecoveryKey) : .success(recoveryKey) } catch { return .failure(.failedGeneratingRecoveryKey) } @@ -119,7 +118,7 @@ class SecureBackupController: SecureBackupControllerProtocol { func confirmRecoveryKey(_ key: String) async -> Result { do { - try await encryption.fixRecoveryIssues(recoveryKey: key) + try await encryption.recover(recoveryKey: key) return .success(()) } catch { return .failure(.failedConfirmingRecoveryKey) @@ -143,7 +142,7 @@ class SecureBackupController: SecureBackupControllerProtocol { case .BackupDisabled: MXLog.error("Key backup disabled, continuing logout.") return .success(()) - case .Connection, .Laged: + case .Connection, .Lagged: MXLog.error("Key backup upload failure: \(error)") return .failure(.failedUploadingForBackup) } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index cd2230af1..48f4a7b69 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -52,7 +52,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result { guard let roomProxy, let eventID = itemID.eventID else { return .failure(.generic) } - switch await roomProxy.sendReadReceipt(for: eventID) { + switch await roomProxy.timeline.sendReadReceipt(for: eventID) { case .success: return .success(()) case .failure: @@ -89,7 +89,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { return } - await roomProxy?.retrySend(transactionID: transactionID) + await roomProxy?.timeline.retrySend(transactionID: transactionID) } func cancelSending(itemID: TimelineItemIdentifier) async { @@ -97,7 +97,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { return } - await roomProxy?.cancelSend(transactionID: transactionID) + await roomProxy?.timeline.cancelSend(transactionID: transactionID) } // MARK: - UI Test signalling diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index b71c0e71c..bb39191a1 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -46,7 +46,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { appSettings: AppSettings, secureBackupController: SecureBackupControllerProtocol) { self.roomProxy = roomProxy - timelineProvider = roomProxy.timelineProvider + timelineProvider = roomProxy.timeline.timelineProvider self.timelineItemFactory = timelineItemFactory self.appSettings = appSettings self.secureBackupController = secureBackupController @@ -71,13 +71,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol { func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { MXLog.info("Started back pagination request") - switch await roomProxy.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) { + switch await roomProxy.timeline.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) { case .success: MXLog.info("Finished back pagination request") return .success(()) - case .failure(.noMoreMessagesToBackPaginate): - MXLog.warning("Back pagination requested when all messages have been loaded.") - return .success(()) case .failure(let error): MXLog.error("Failed back pagination request with error: \(error)") return .failure(.generic) @@ -89,7 +86,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { let eventID = itemID.eventID else { return .success(()) } - switch await roomProxy.sendReadReceipt(for: eventID) { + switch await roomProxy.timeline.sendReadReceipt(for: eventID) { case .success: return .success(()) case .failure: @@ -123,11 +120,11 @@ class RoomTimelineController: RoomTimelineControllerProtocol { MXLog.error("Send reply in \(roomID) failed: missing event ID") return } - - switch await roomProxy.sendMessage(message, - html: html, - inReplyTo: inReplyTo, - intentionalMentions: intentionalMentions) { + + switch await roomProxy.timeline.sendMessage(message, + html: html, + inReplyTo: inReplyTo, + intentionalMentions: intentionalMentions) { case .success: MXLog.info("Finished sending message") case .failure(let error): @@ -142,7 +139,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return } - switch await roomProxy.toggleReaction(reaction, to: eventID) { + switch await roomProxy.timeline.toggleReaction(reaction, to: eventID) { case .success: MXLog.info("Finished toggling reaction") case .failure(let error): @@ -162,10 +159,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol { await cancelSending(itemID: itemID) await sendMessage(newMessage, html: html, intentionalMentions: intentionalMentions) } else if let eventID = itemID.eventID { - switch await roomProxy.editMessage(newMessage, - html: html, - original: eventID, - intentionalMentions: intentionalMentions) { + switch await roomProxy.timeline.editMessage(newMessage, + html: html, + original: eventID, + intentionalMentions: intentionalMentions) { case .success: MXLog.info("Finished editing message") case .failure(let error): @@ -207,7 +204,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } func retryDecryption(for sessionID: String) async { - await roomProxy.retryDecryption(for: sessionID) + await roomProxy.timeline.retryDecryption(for: sessionID) } func retrySending(itemID: TimelineItemIdentifier) async { @@ -217,7 +214,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } MXLog.info("Retry sending in \(roomID)") - await roomProxy.retrySend(transactionID: transactionID) + await roomProxy.timeline.retrySend(transactionID: transactionID) } func cancelSending(itemID: TimelineItemIdentifier) async { @@ -227,7 +224,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } MXLog.info("Cancelling send in \(roomID)") - await roomProxy.cancelSend(transactionID: transactionID) + await roomProxy.timeline.cancelSend(transactionID: transactionID) } // MARK: - Private @@ -378,10 +375,10 @@ class RoomTimelineController: RoomTimelineControllerProtocol { switch timelineItem.replyDetails { case .notLoaded: - roomProxy.fetchDetails(for: eventID) + roomProxy.timeline.fetchDetails(for: eventID) case .error: if refetchOnError { - roomProxy.fetchDetails(for: eventID) + roomProxy.timeline.fetchDetails(for: eventID) } default: break diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift new file mode 100644 index 000000000..3fdf9c6d8 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -0,0 +1,518 @@ +// +// Copyright 2023 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. +// + +import Combine +import Foundation +import MatrixRustSDK + +final class TimelineProxy: TimelineProxyProtocol { + private let timeline: Timeline + private var sendMessageBackgroundTask: BackgroundTaskProtocol? + private let backgroundTaskService: BackgroundTaskServiceProtocol + + private let backgroundTaskName = "SendRoomEvent" + private let lowPriorityDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.low_priority", qos: .utility) + private let messageSendingDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.message_sending", qos: .userInitiated) + private let userInitiatedDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.user_initiated", qos: .userInitiated) + + init(timeline: Timeline, backgroundTaskService: BackgroundTaskServiceProtocol) { + self.timeline = timeline + self.backgroundTaskService = backgroundTaskService + } + + private var backPaginationStateObservationToken: TaskHandle? + private var roomTimelineObservationToken: TaskHandle? + private var timelineListener: RoomTimelineListener? + + private let backPaginationStateSubject = PassthroughSubject() + private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>() + + private var innerTimelineProvider: RoomTimelineProviderProtocol! + var timelineProvider: RoomTimelineProviderProtocol { + innerTimelineProvider + } + + var hasPendingUpdatesSubscription: Bool { + innerTimelineProvider != nil + } + + deinit { + backPaginationStateObservationToken?.cancel() + roomTimelineObservationToken?.cancel() + } + + func subscribeForUpdates() async { + guard innerTimelineProvider == nil else { + MXLog.warning("Timeline already subscribed for updates") + return + } + + let timelineListener = RoomTimelineListener { [weak self] timelineDiffs in + self?.timelineUpdatesSubject.send(timelineDiffs) + } + + self.timelineListener = timelineListener + + let result = await timeline.addListener(listener: timelineListener) + roomTimelineObservationToken = result.itemsStream + + subscribeToBackpagination() + + innerTimelineProvider = await RoomTimelineProvider(currentItems: result.items, + updatePublisher: timelineUpdatesSubject.eraseToAnyPublisher(), + backPaginationStatePublisher: backPaginationStateSubject.eraseToAnyPublisher()) + } + + func cancelSend(transactionID: String) async { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: messageSendingDispatchQueue) { + self.timeline.cancelSend(txnId: transactionID) + } + } + + func editMessage(_ message: String, + html: String?, + original eventID: String, + intentionalMentions: IntentionalMentions) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let messageContent = buildMessageContentFor(message, + html: html, + intentionalMentions: intentionalMentions.toRustMentions()) + + return await Task.dispatch(on: messageSendingDispatchQueue) { + do { + let originalEvent = try self.timeline.getEventTimelineItemByEventId(eventId: eventID) + try self.timeline.edit(newContent: messageContent, editItem: originalEvent) + return .success(()) + } catch { + return .failure(.failedEditingMessage) + } + } + } + + func fetchDetails(for eventID: String) { + Task { + await Task.dispatch(on: .global()) { + do { + MXLog.info("Fetching event details for \(eventID)") + try self.timeline.fetchDetailsForEvent(eventId: eventID) + } catch { + MXLog.error("Failed fetching event details for \(eventID) with error: \(error)") + } + } + } + } + + func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? { + try? timeline.getTimelineEventContentByEventId(eventId: eventID) + } + + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result { + do { + try await Task.dispatch(on: .global()) { + try self.timeline.paginateBackwards(opts: .untilNumItems(eventLimit: UInt16(requestSize), items: UInt16(untilNumberOfItems), waitForToken: true)) + } + + return .success(()) + } catch { + return .failure(.failedPaginatingBackwards) + } + } + + func retryDecryption(for sessionID: String) async { + await Task.dispatch(on: .global()) { [weak self] in + self?.timeline.retryDecryption(sessionIds: [sessionID]) + } + } + + func retrySend(transactionID: String) async { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: messageSendingDispatchQueue) { + self.timeline.retrySend(txnId: transactionID) + } + } + + func sendAudio(url: URL, + audioInfo: AudioInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let handle = timeline.sendAudio(url: url.path(percentEncoded: false), audioInfo: audioInfo, progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }) + + await requestHandle(handle) + + do { + try await handle.join() + } catch { + return .failure(.failedSendingMedia) + } + + return .success(()) + } + + func sendFile(url: URL, + fileInfo: FileInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let handle = timeline.sendFile(url: url.path(percentEncoded: false), fileInfo: fileInfo, progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }) + + await requestHandle(handle) + + do { + try await handle.join() + } catch { + return .failure(.failedSendingMedia) + } + + return .success(()) + } + + func sendImage(url: URL, + thumbnailURL: URL, + imageInfo: ImageInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let handle = timeline.sendImage(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), imageInfo: imageInfo, progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }) + + await requestHandle(handle) + + do { + try await handle.join() + } catch { + return .failure(.failedSendingMedia) + } + + return .success(()) + } + + func sendLocation(body: String, + geoURI: GeoURI, + description: String?, + zoomLevel: UInt8?, + assetType: AssetType?) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: messageSendingDispatchQueue) { + .success(self.timeline.sendLocation(body: body, + geoUri: geoURI.string, + description: description, + zoomLevel: zoomLevel, + assetType: assetType)) + } + } + + func sendVideo(url: URL, + thumbnailURL: URL, + videoInfo: VideoInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let handle = timeline.sendVideo(url: url.path(percentEncoded: false), thumbnailUrl: thumbnailURL.path(percentEncoded: false), videoInfo: videoInfo, progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }) + + await requestHandle(handle) + + do { + try await handle.join() + } catch { + return .failure(.failedSendingMedia) + } + + return .success(()) + } + + func sendVoiceMessage(url: URL, + audioInfo: AudioInfo, + waveform: [UInt16], + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let handle = timeline.sendVoiceMessage(url: url.path(percentEncoded: false), audioInfo: audioInfo, waveform: waveform, progressWatcher: UploadProgressListener { progress in + progressSubject?.send(progress) + }) + + await requestHandle(handle) + + do { + try await handle.join() + } catch { + return .failure(.failedSendingMedia) + } + + return .success(()) + } + + func sendMessage(_ message: String, + html: String?, + inReplyTo eventID: String? = nil, + intentionalMentions: IntentionalMentions) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + let messageContent = buildMessageContentFor(message, + html: html, + intentionalMentions: intentionalMentions.toRustMentions()) + + return await Task.dispatch(on: messageSendingDispatchQueue) { + do { + if let eventID { + let replyItem = try self.timeline.getEventTimelineItemByEventId(eventId: eventID) + try self.timeline.sendReply(msg: messageContent, replyItem: replyItem) + } else { + self.timeline.send(msg: messageContent) + } + } catch { + return .failure(.failedSendingMessage) + } + return .success(()) + } + } + + func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: messageSendingDispatchQueue) { + self.timeline.send(msg: messageContent) + return .success(()) + } + } + + func sendReadReceipt(for eventID: String) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: lowPriorityDispatchQueue) { + do { + try self.timeline.sendReadReceipt(eventId: eventID) + return .success(()) + } catch { + return .failure(.failedSendingReadReceipt) + } + } + } + + func toggleReaction(_ reaction: String, to eventID: String) async -> Result { + sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: userInitiatedDispatchQueue) { + do { + try self.timeline.toggleReaction(eventId: eventID, key: reaction) + return .success(()) + } catch { + return .failure(.failedSendingReaction) + } + } + } + + // MARK: - Polls + + func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { + await Task.dispatch(on: .global()) { + do { + return try .success(self.timeline.createPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind))) + } catch { + MXLog.error("Failed creating a poll: \(error)") + return .failure(.failedCreatingPoll) + } + } + } + + func editPoll(original eventID: String, + question: String, + answers: [String], + pollKind: Poll.Kind) async -> Result { + do { + let originalEvent = try await Task.dispatch(on: .global()) { + try self.timeline.getEventTimelineItemByEventId(eventId: eventID) + } + return try await .success(timeline.editPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind), editItem: originalEvent)) + } catch { + MXLog.error("Failed editing the poll: \(error), eventID: \(eventID)") + return .failure(.failedEditingPoll) + } + } + + func endPoll(pollStartID: String, text: String) async -> Result { + await Task.dispatch(on: .global()) { + do { + return try .success(self.timeline.endPoll(pollStartId: pollStartID, text: text)) + } catch { + MXLog.error("Failed ending a poll: \(error), pollStartID: \(pollStartID)") + return .failure(.failedEndingPoll) + } + } + } + + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { + await Task.dispatch(on: .global()) { + do { + return try .success(self.timeline.sendPollResponse(pollStartId: pollStartID, answers: answers)) + } catch { + MXLog.error("Failed sending a poll vote: \(error), pollStartID: \(pollStartID)") + return .failure(.failedSendingPollResponse) + } + } + } + + // MARK: - Private + + private func buildMessageContentFor(_ message: String, + html: String?, + intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation { + let emoteSlashCommand = "/me " + let isEmote: Bool = message.starts(with: emoteSlashCommand) + + let content: RoomMessageEventContentWithoutRelation + if isEmote { + let emoteMessage = String(message.dropFirst(emoteSlashCommand.count)) + + var emoteHtml: String? + if let html { + emoteHtml = String(html.dropFirst(emoteSlashCommand.count)) + } + content = buildEmoteMessageContentFor(emoteMessage, html: emoteHtml) + } else { + if let html { + content = messageEventContentFromHtml(body: message, htmlBody: html) + } else { + content = messageEventContentFromMarkdown(md: message) + } + } + return content.withMentions(mentions: intentionalMentions) + } + + private func buildEmoteMessageContentFor(_ message: String, html: String?) -> RoomMessageEventContentWithoutRelation { + if let html { + return messageEventContentFromHtmlAsEmote(body: message, htmlBody: html) + } else { + return messageEventContentFromMarkdownAsEmote(md: message) + } + } + + private func subscribeToBackpagination() { + let listener = RoomBackpaginationStatusListener { [weak self] status in + self?.backPaginationStateSubject.send(status) + } + do { + backPaginationStateObservationToken = try timeline.subscribeToBackPaginationStatus(listener: listener) + } catch { + MXLog.error("Failed to subscribe to back pagination state with error: \(error)") + } + } +} + +private final class RoomTimelineListener: TimelineListener { + private let onUpdateClosure: ([TimelineDiff]) -> Void + + init(_ onUpdateClosure: @escaping ([TimelineDiff]) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func onUpdate(diff: [TimelineDiff]) { + onUpdateClosure(diff) + } +} + +private final class RoomBackpaginationStatusListener: BackPaginationStatusListener { + private let onUpdateClosure: (BackPaginationStatus) -> Void + + init(_ onUpdateClosure: @escaping (BackPaginationStatus) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func onUpdate(status: BackPaginationStatus) { + onUpdateClosure(status) + } +} + +private final class UploadProgressListener: ProgressWatcher { + private let onUpdateClosure: (Double) -> Void + + init(_ onUpdateClosure: @escaping (Double) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func transmissionProgress(progress: TransmissionProgress) { + DispatchQueue.main.async { [weak self] in + self?.onUpdateClosure(Double(progress.current) / Double(progress.total)) + } + } +} + +private extension MatrixRustSDK.PollKind { + init(pollKind: Poll.Kind) { + switch pollKind { + case .disclosed: + self = .disclosed + case .undisclosed: + self = .undisclosed + } + } +} diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift new file mode 100644 index 000000000..1081dced5 --- /dev/null +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -0,0 +1,127 @@ +// +// Copyright 2023 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. +// + +import Combine +import Foundation +import MatrixRustSDK + +enum TimelineProxyError: Error, Equatable { + case failedEditingMessage + case failedPaginatingBackwards + case failedSendingMessage + case failedSendingReaction + case failedSendingReadReceipt + case failedSendingMedia + + // Polls + case failedCreatingPoll + case failedEditingPoll + case failedEndingPoll + case failedSendingPollResponse +} + +// sourcery: AutoMockable +protocol TimelineProxyProtocol { + var timelineProvider: RoomTimelineProviderProtocol { get } + func subscribeForUpdates() async + + /// Cancels a failed message given its transaction ID from the timeline + func cancelSend(transactionID: String) async + + func editMessage(_ message: String, + html: String?, + original eventID: String, + intentionalMentions: IntentionalMentions) async -> Result + + func fetchDetails(for eventID: String) + + func messageEventContent(for eventID: String) -> RoomMessageEventContentWithoutRelation? + + func retryDecryption(for sessionID: String) async + + /// Retries sending a failed message given its transaction ID + func retrySend(transactionID: String) async + + func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result + + func sendAudio(url: URL, + audioInfo: AudioInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result + + func sendFile(url: URL, + fileInfo: FileInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result + + func sendImage(url: URL, + thumbnailURL: URL, + imageInfo: ImageInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result + + func sendLocation(body: String, + geoURI: GeoURI, + description: String?, + zoomLevel: UInt8?, + assetType: AssetType?) async -> Result + + func sendVideo(url: URL, + thumbnailURL: URL, + videoInfo: VideoInfo, + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result + + func sendVoiceMessage(url: URL, + audioInfo: AudioInfo, + waveform: [UInt16], + progressSubject: CurrentValueSubject?, + requestHandle: @MainActor (SendAttachmentJoinHandleProtocol) -> Void) async -> Result + + func sendReadReceipt(for eventID: String) async -> Result + + func sendMessageEventContent(_ messageContent: RoomMessageEventContentWithoutRelation) async -> Result + + func sendMessage(_ message: String, + html: String?, + inReplyTo eventID: String?, + intentionalMentions: IntentionalMentions) async -> Result + + func toggleReaction(_ reaction: String, to eventID: String) async -> Result + + // Polls + func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result + + func editPoll(original eventID: String, + question: String, + answers: [String], + pollKind: Poll.Kind) async -> Result + + func endPoll(pollStartID: String, text: String) async -> Result + + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result +} + +extension TimelineProxyProtocol { + func sendMessage(_ message: String, + html: String?, + intentionalMentions: IntentionalMentions) async -> Result { + await sendMessage(message, + html: html, + inReplyTo: nil, + intentionalMentions: intentionalMentions) + } +} diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift index a41ebbaa5..80c4f1e50 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift @@ -190,10 +190,10 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { return .failure(.failedSendingVoiceMessage) } - let result = await roomProxy.sendVoiceMessage(url: oggFile, - audioInfo: audioInfo, - waveform: waveform, - progressSubject: nil) { _ in } + let result = await roomProxy.timeline.sendVoiceMessage(url: oggFile, + audioInfo: audioInfo, + waveform: waveform, + progressSubject: nil) { _ in } if case .failure(let error) = result { MXLog.error("Failed to send the voice message. \(error)") diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 93f491b84..6534359b4 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -332,6 +332,8 @@ class RoomScreenViewModelTests: XCTestCase { func testRetrySend() async throws { let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + let timelineProxy = TimelineProxyMock() + roomProxyMock.timeline = timelineProxy timelineController.roomProxy = roomProxyMock let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, @@ -349,13 +351,15 @@ class RoomScreenViewModelTests: XCTestCase { try? await Task.sleep(for: .milliseconds(100)) - XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 1) - XCTAssert(roomProxyMock.retrySendTransactionIDReceivedInvocations == ["test retry send id"]) + XCTAssert(timelineProxy.retrySendTransactionIDCallsCount == 1) + XCTAssert(timelineProxy.retrySendTransactionIDReceivedInvocations == ["test retry send id"]) } func testRetrySendNoTransactionID() async { let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + let timelineProxy = TimelineProxyMock() + roomProxyMock.timeline = timelineProxy let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, timelineController: timelineController, @@ -372,12 +376,14 @@ class RoomScreenViewModelTests: XCTestCase { try? await Task.sleep(for: .milliseconds(100)) - XCTAssert(roomProxyMock.retrySendTransactionIDCallsCount == 0) + XCTAssert(timelineProxy.retrySendTransactionIDCallsCount == 0) } func testCancelSend() async { let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + let timelineProxy = TimelineProxyMock() + roomProxyMock.timeline = timelineProxy timelineController.roomProxy = roomProxyMock let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, @@ -395,13 +401,15 @@ class RoomScreenViewModelTests: XCTestCase { try? await Task.sleep(for: .milliseconds(100)) - XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 1) - XCTAssert(roomProxyMock.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"]) + XCTAssert(timelineProxy.cancelSendTransactionIDCallsCount == 1) + XCTAssert(timelineProxy.cancelSendTransactionIDReceivedInvocations == ["test cancel send id"]) } func testCancelSendNoTransactionID() async { let timelineController = MockRoomTimelineController() let roomProxyMock = RoomProxyMock(with: .init(displayName: "")) + let timelineProxy = TimelineProxyMock() + roomProxyMock.timeline = timelineProxy let viewModel = RoomScreenViewModel(roomProxy: roomProxyMock, timelineController: timelineController, @@ -418,7 +426,7 @@ class RoomScreenViewModelTests: XCTestCase { try? await Task.sleep(for: .milliseconds(100)) - XCTAssert(roomProxyMock.cancelSendTransactionIDCallsCount == 0) + XCTAssert(timelineProxy.cancelSendTransactionIDCallsCount == 0) } // MARK: - Read Receipts @@ -429,15 +437,15 @@ class RoomScreenViewModelTests: XCTestCase { let items = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t3")] - let (viewModel, roomProxy, _, notificationCenter) = readReceiptsConfiguration(with: items) + let (viewModel, roomProxy, timelineProxy, _, notificationCenter) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) // Then the receipt should be sent. - XCTAssertEqual(roomProxy.sendReadReceiptForCalled, true) - XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t3") + XCTAssertEqual(timelineProxy.sendReadReceiptForCalled, true) + XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t3") // And the notifications should be cleared. XCTAssertEqual(notificationCenter.postNameObjectReceivedArguments?.aName, .roomMarkedAsRead) @@ -450,19 +458,19 @@ class RoomScreenViewModelTests: XCTestCase { let items = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), TextRoomTimelineItem(eventID: "t3")] - let (viewModel, roomProxy, timelineController, _) = readReceiptsConfiguration(with: items) + let (viewModel, _, timelineProxy, timelineController, _) = readReceiptsConfiguration(with: items) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) - XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) - XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t3") + XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) + XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t3") // When sending a receipt for the first item in the timeline. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.first!.id)) try await Task.sleep(for: .milliseconds(100)) // Then the request should be ignored. - XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) - XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t3") + XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) + XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t3") // When a new message is received and marked as read. let newMessage = TextRoomTimelineItem(eventID: "t4") @@ -474,8 +482,8 @@ class RoomScreenViewModelTests: XCTestCase { try await Task.sleep(for: .milliseconds(100)) // Then the request should be made. - XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 2) - XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t4") + XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 2) + XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t4") } func testSendReadReceiptWithoutEvents() async throws { @@ -483,14 +491,14 @@ class RoomScreenViewModelTests: XCTestCase { let items = [SeparatorRoomTimelineItem(timelineID: "v1"), SeparatorRoomTimelineItem(timelineID: "v2"), SeparatorRoomTimelineItem(timelineID: "v3")] - let (viewModel, roomProxy, _, _) = readReceiptsConfiguration(with: items) + let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) // Then nothing should be sent. - XCTAssertEqual(roomProxy.sendReadReceiptForCalled, false) + XCTAssertEqual(timelineProxy.sendReadReceiptForCalled, false) } func testSendReadReceiptVirtualLast() async throws { @@ -498,15 +506,15 @@ class RoomScreenViewModelTests: XCTestCase { let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), SeparatorRoomTimelineItem(timelineID: "v3")] - let (viewModel, roomProxy, _, _) = readReceiptsConfiguration(with: items) + let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) // Then a read receipt should be sent for the item before it. - XCTAssertEqual(roomProxy.sendReadReceiptForCalled, true) - XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t2") + XCTAssertEqual(timelineProxy.sendReadReceiptForCalled, true) + XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t2") } func testSendReadReceiptMultipleRequests() async throws { @@ -514,31 +522,34 @@ class RoomScreenViewModelTests: XCTestCase { let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), SeparatorRoomTimelineItem(timelineID: "v3")] - let (viewModel, roomProxy, _, _) = readReceiptsConfiguration(with: items) + let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items) viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) - XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) - XCTAssertEqual(roomProxy.sendReadReceiptForReceivedEventID, "t2") + XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) + XCTAssertEqual(timelineProxy.sendReadReceiptForReceivedEventID, "t2") // When sending the same receipt again viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) try await Task.sleep(for: .milliseconds(100)) // Then the second call should be ignored. - XCTAssertEqual(roomProxy.sendReadReceiptForCallsCount, 1) + XCTAssertEqual(timelineProxy.sendReadReceiptForCallsCount, 1) } // swiftlint:enable force_unwrapping // swiftlint:disable:next large_tuple private func readReceiptsConfiguration(with items: [RoomTimelineItemProtocol]) -> (RoomScreenViewModel, RoomProxyMock, + TimelineProxyMock, MockRoomTimelineController, NotificationCenterMock) { let notificationCenter = NotificationCenterMock() let roomProxy = RoomProxyMock(with: .init(displayName: "")) + let timelineProxy = TimelineProxyMock() + roomProxy.timeline = timelineProxy let timelineController = MockRoomTimelineController() - roomProxy.sendReadReceiptForReturnValue = .success(()) + timelineProxy.sendReadReceiptForReturnValue = .success(()) roomProxy.underlyingHasUnreadNotifications = true timelineController.timelineItems = items timelineController.roomProxy = roomProxy @@ -554,7 +565,7 @@ class RoomScreenViewModelTests: XCTestCase { analyticsService: ServiceLocator.shared.analytics, notificationCenter: notificationCenter) - return (viewModel, roomProxy, timelineController, notificationCenter) + return (viewModel, roomProxy, timelineProxy, timelineController, notificationCenter) } func testShowReadReceipts() async throws { diff --git a/UnitTests/Sources/VoiceMessageRecorderTests.swift b/UnitTests/Sources/VoiceMessageRecorderTests.swift index 31285fbce..e6a26ff6e 100644 --- a/UnitTests/Sources/VoiceMessageRecorderTests.swift +++ b/UnitTests/Sources/VoiceMessageRecorderTests.swift @@ -237,8 +237,10 @@ class VoiceMessageRecorderTests: XCTestCase { try? FileManager.default.removeItem(at: destination) } + let timelineProxy = TimelineProxyMock() let roomProxy = RoomProxyMock() - roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.failedSendingMedia) + roomProxy.timeline = timelineProxy + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.failedSendingMedia) guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { XCTFail("An error is expected") return @@ -256,8 +258,10 @@ class VoiceMessageRecorderTests: XCTestCase { try? FileManager.default.copyItem(at: imageFileURL, to: destination) } + let timelineProxy = TimelineProxyMock() let roomProxy = RoomProxyMock() - roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.failedSendingMedia) + roomProxy.timeline = timelineProxy + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.failedSendingMedia) guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { XCTFail("An error is expected") return @@ -277,8 +281,10 @@ class VoiceMessageRecorderTests: XCTestCase { } // If the media upload fails + let timelineProxy = TimelineProxyMock() let roomProxy = RoomProxyMock() - roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.failedSendingMedia) + roomProxy.timeline = timelineProxy + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleReturnValue = .failure(.failedSendingMedia) guard case .failure(.failedSendingVoiceMessage) = await voiceMessageRecorder.sendVoiceMessage(inRoom: roomProxy, audioConverter: audioConverter) else { XCTFail("An error is expected") return @@ -291,7 +297,9 @@ class VoiceMessageRecorderTests: XCTestCase { return } + let timelineProxy = TimelineProxyMock() let roomProxy = RoomProxyMock() + roomProxy.timeline = timelineProxy audioRecorder.currentTime = 42 audioRecorder.audioFileURL = imageFileURL _ = await voiceMessageRecorder.startRecording() @@ -312,7 +320,7 @@ class VoiceMessageRecorderTests: XCTestCase { XCTAssertEqual(destination.pathExtension, "ogg") } - roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = { url, audioInfo, waveform, _, _ in + timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleClosure = { url, audioInfo, waveform, _, _ in XCTAssertEqual(url, convertedFileURL) XCTAssertEqual(audioInfo.duration, self.audioRecorder.currentTime) XCTAssertEqual(audioInfo.size, convertedFileSize) @@ -328,7 +336,7 @@ class VoiceMessageRecorderTests: XCTestCase { } XCTAssert(audioConverter.convertToOpusOggSourceURLDestinationURLCalled) - XCTAssert(roomProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled) + XCTAssert(timelineProxy.sendVoiceMessageUrlAudioInfoWaveformProgressSubjectRequestHandleCalled) // the converted file must have been deleted if let convertedFileURL { diff --git a/project.yml b/project.yml index 382afecf0..ebd3d6859 100644 --- a/project.yml +++ b/project.yml @@ -45,7 +45,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 0.0.6-november23 + exactVersion: 0.0.7-november23 # path: ../matrix-rust-sdk Compound: url: https://github.com/vector-im/compound-ios