diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index f5abd3f0c..0f5da4015 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1939,11 +1939,11 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingActiveMembersCount = value } } var underlyingActiveMembersCount: Int! - var stateUpdatesPublisher: AnyPublisher { - get { return underlyingStateUpdatesPublisher } - set(value) { underlyingStateUpdatesPublisher = value } + var actions: AnyPublisher { + get { return underlyingActions } + set(value) { underlyingActions = value } } - var underlyingStateUpdatesPublisher: AnyPublisher! + var underlyingActions: AnyPublisher! var timeline: TimelineProxyProtocol { get { return underlyingTimeline } set(value) { underlyingTimeline = value } @@ -2578,6 +2578,11 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy } } class TimelineProxyMock: TimelineProxyProtocol { + var actions: AnyPublisher { + get { return underlyingActions } + set(value) { underlyingActions = value } + } + var underlyingActions: AnyPublisher! var timelineProvider: RoomTimelineProviderProtocol { get { return underlyingTimelineProvider } set(value) { underlyingTimelineProvider = value } diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index f322a34a0..29e4a3385 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -32,12 +32,19 @@ struct RoomProxyMockConfiguration { var canonicalAlias: String? var alternativeAliases: [String] = [] var hasUnreadNotifications = Bool.random() + var canUserTriggerRoomNotification = false + + var timeline = { + let mock = TimelineProxyMock() + mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher() + return mock + }() + var members: [RoomMemberProxyProtocol]? var inviter: RoomMemberProxyMock? var memberForID: RoomMemberProxyMock = .mockMe var ownUserID = "@alice:somewhere.org" - var canUserTriggerRoomNotification = false - + var invitedMembersCount = 100 var joinedMembersCount = 50 var activeMembersCount = 25 @@ -62,6 +69,8 @@ extension RoomProxyMock { alternativeAliases = configuration.alternativeAliases hasUnreadNotifications = configuration.hasUnreadNotifications + timeline = configuration.timeline + invitedMembersCount = configuration.invitedMembersCount joinedMembersCount = configuration.joinedMembersCount activeMembersCount = configuration.activeMembersCount @@ -79,7 +88,7 @@ extension RoomProxyMock { updateMembersClosure = { } acceptInvitationClosure = { .success(()) } - underlyingStateUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() + underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher() setNameClosure = { _ in .success(()) } setTopicClosure = { _ in .success(()) } getMemberUserIDReturnValue = .success(configuration.memberForID) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 242a9c63b..25971f003 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -120,7 +120,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr // MARK: - Private private func setupRoomSubscription() { - roomProxy.stateUpdatesPublisher + roomProxy.actions + .filter { $0 == .stateUpdate } .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in guard let self else { return } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 3118b9531..4acdaa96c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -279,7 +279,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .store(in: &cancellables) roomProxy - .stateUpdatesPublisher + .actions + .filter { $0 == .stateUpdate } .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in guard let self else { return } @@ -288,6 +289,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol self.state.hasOngoingCall = roomProxy.hasOngoingCall } .store(in: &cancellables) + + roomProxy.timeline.actions + .filter { $0 == .sentMessage } + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.state.timelineViewState.scrollToBottomPublisher.send(()) + } + .store(in: &cancellables) appSettings.$timelineStyle .weakAssign(to: \.state.timelineStyle, on: self) @@ -468,8 +477,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .recordVoiceMessage, .previewVoiceMessage: fatalError("invalid composer mode.") } - - state.timelineViewState.scrollToBottomPublisher.send(()) } private func trackComposerMode(_ mode: RoomScreenComposerMode) { diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index a54e6fea7..c17c4b4e3 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -42,9 +42,9 @@ class RoomProxy: RoomProxyProtocol { membersSubject.asCurrentValuePublisher() } - private let stateUpdatesSubject = PassthroughSubject() - var stateUpdatesPublisher: AnyPublisher { - stateUpdatesSubject.eraseToAnyPublisher() + private let actionsSubject = PassthroughSubject() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() } var ownUserID: String { @@ -407,7 +407,7 @@ class RoomProxy: RoomProxyProtocol { private func subscribeToRoomStateUpdates() { roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in MXLog.info("Received room info update") - self?.stateUpdatesSubject.send(()) + self?.actionsSubject.send(.stateUpdate) }) } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index c7f447e6c..7fb6cb345 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -35,6 +35,10 @@ enum RoomProxyError: Error, Equatable { case failedCheckingPermission } +enum RoomProxyAction { + case stateUpdate +} + // sourcery: AutoMockable protocol RoomProxyProtocol { var id: String { get } @@ -65,9 +69,7 @@ protocol RoomProxyProtocol { var activeMembersCount: Int { get } - /// Publishes room state updates - /// The thread on which this publisher sends the output isn't defined. - var stateUpdatesPublisher: AnyPublisher { get } + var actions: AnyPublisher { get } var timeline: TimelineProxyProtocol { get } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 3fdf9c6d8..cb3328c1b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -28,17 +28,17 @@ final class TimelineProxy: TimelineProxyProtocol { 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 let actionsSubject = PassthroughSubject() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } private var innerTimelineProvider: RoomTimelineProviderProtocol! var timelineProvider: RoomTimelineProviderProtocol { @@ -54,6 +54,11 @@ final class TimelineProxy: TimelineProxyProtocol { roomTimelineObservationToken?.cancel() } + init(timeline: Timeline, backgroundTaskService: BackgroundTaskServiceProtocol) { + self.timeline = timeline + self.backgroundTaskService = backgroundTaskService + } + func subscribeForUpdates() async { guard innerTimelineProvider == nil else { MXLog.warning("Timeline already subscribed for updates") @@ -104,6 +109,7 @@ final class TimelineProxy: TimelineProxyProtocol { do { let originalEvent = try self.timeline.getEventTimelineItemByEventId(eventId: eventID) try self.timeline.edit(newContent: messageContent, editItem: originalEvent) + return .success(()) } catch { return .failure(.failedEditingMessage) @@ -178,6 +184,8 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.failedSendingMedia) } + actionsSubject.send(.sentMessage) + return .success(()) } @@ -202,6 +210,8 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.failedSendingMedia) } + actionsSubject.send(.sentMessage) + return .success(()) } @@ -227,6 +237,8 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.failedSendingMedia) } + actionsSubject.send(.sentMessage) + return .success(()) } @@ -241,11 +253,15 @@ final class TimelineProxy: TimelineProxyProtocol { } return await Task.dispatch(on: messageSendingDispatchQueue) { - .success(self.timeline.sendLocation(body: body, - geoUri: geoURI.string, - description: description, - zoomLevel: zoomLevel, - assetType: assetType)) + self.timeline.sendLocation(body: body, + geoUri: geoURI.string, + description: description, + zoomLevel: zoomLevel, + assetType: assetType) + + self.actionsSubject.send(.sentMessage) + + return .success(()) } } @@ -271,6 +287,8 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.failedSendingMedia) } + actionsSubject.send(.sentMessage) + return .success(()) } @@ -296,6 +314,8 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.failedSendingMedia) } + actionsSubject.send(.sentMessage) + return .success(()) } @@ -323,6 +343,9 @@ final class TimelineProxy: TimelineProxyProtocol { } catch { return .failure(.failedSendingMessage) } + + self.actionsSubject.send(.sentMessage) + return .success(()) } } @@ -335,6 +358,9 @@ final class TimelineProxy: TimelineProxyProtocol { return await Task.dispatch(on: messageSendingDispatchQueue) { self.timeline.send(msg: messageContent) + + self.actionsSubject.send(.sentMessage) + return .success(()) } } @@ -376,7 +402,11 @@ final class TimelineProxy: TimelineProxyProtocol { 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))) + try self.timeline.createPoll(question: question, answers: answers, maxSelections: 1, pollKind: .init(pollKind: pollKind)) + + self.actionsSubject.send(.sentMessage) + + return .success(()) } catch { MXLog.error("Failed creating a poll: \(error)") return .failure(.failedCreatingPoll) diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 1081dced5..131152ac9 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -33,9 +33,16 @@ enum TimelineProxyError: Error, Equatable { case failedSendingPollResponse } +enum TimelineProxyAction { + case sentMessage +} + // sourcery: AutoMockable protocol TimelineProxyProtocol { + var actions: AnyPublisher { get } + var timelineProvider: RoomTimelineProviderProtocol { get } + func subscribeForUpdates() async /// Cancels a failed message given its transaction ID from the timeline diff --git a/UnitTests/Sources/CompletionSuggestionServiceTests.swift b/UnitTests/Sources/CompletionSuggestionServiceTests.swift index b8b260bf9..ec6c95c58 100644 --- a/UnitTests/Sources/CompletionSuggestionServiceTests.swift +++ b/UnitTests/Sources/CompletionSuggestionServiceTests.swift @@ -66,7 +66,7 @@ final class CompletionSuggestionServiceTests: XCTestCase { func testUserSuggestonsIncludingAllUsers() async throws { let alice: RoomMemberProxyMock = .mockAlice let members: [RoomMemberProxyMock] = [alice, .mockBob, .mockCharlie, .mockMe] - let roomProxyMock = RoomProxyMock(with: .init(displayName: "test", members: members, canUserTriggerRoomNotification: true)) + let roomProxyMock = RoomProxyMock(with: .init(displayName: "test", canUserTriggerRoomNotification: true, members: members)) let service = CompletionSuggestionService(roomProxy: roomProxyMock) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in @@ -92,7 +92,7 @@ final class CompletionSuggestionServiceTests: XCTestCase { let alice: RoomMemberProxyMock = .mockAlice let bob: RoomMemberProxyMock = .mockBob let members: [RoomMemberProxyMock] = [alice, bob, .mockMe] - let roomProxyMock = RoomProxyMock(with: .init(displayName: "test", members: members, canUserTriggerRoomNotification: true)) + let roomProxyMock = RoomProxyMock(with: .init(displayName: "test", canUserTriggerRoomNotification: true, members: members)) let service = CompletionSuggestionService(roomProxy: roomProxyMock) var deferred = deferFulfillment(service.suggestionsPublisher) { suggestions in