Fixes #2147 - Scroll the timeline to the bottom when sending message as well as media, polls and locations.

This commit is contained in:
Stefan Ceriu
2023-12-04 15:55:28 +02:00
committed by Stefan Ceriu
parent 6905ac5f70
commit 8189b0a471
9 changed files with 92 additions and 31 deletions
@@ -1939,11 +1939,11 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingActiveMembersCount = value }
}
var underlyingActiveMembersCount: Int!
var stateUpdatesPublisher: AnyPublisher<Void, Never> {
get { return underlyingStateUpdatesPublisher }
set(value) { underlyingStateUpdatesPublisher = value }
var actions: AnyPublisher<RoomProxyAction, Never> {
get { return underlyingActions }
set(value) { underlyingActions = value }
}
var underlyingStateUpdatesPublisher: AnyPublisher<Void, Never>!
var underlyingActions: AnyPublisher<RoomProxyAction, Never>!
var timeline: TimelineProxyProtocol {
get { return underlyingTimeline }
set(value) { underlyingTimeline = value }
@@ -2578,6 +2578,11 @@ class SessionVerificationControllerProxyMock: SessionVerificationControllerProxy
}
}
class TimelineProxyMock: TimelineProxyProtocol {
var actions: AnyPublisher<TimelineProxyAction, Never> {
get { return underlyingActions }
set(value) { underlyingActions = value }
}
var underlyingActions: AnyPublisher<TimelineProxyAction, Never>!
var timelineProvider: RoomTimelineProviderProtocol {
get { return underlyingTimelineProvider }
set(value) { underlyingTimelineProvider = value }
+12 -3
View File
@@ -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)
@@ -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 }
@@ -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) {
@@ -42,9 +42,9 @@ class RoomProxy: RoomProxyProtocol {
membersSubject.asCurrentValuePublisher()
}
private let stateUpdatesSubject = PassthroughSubject<Void, Never>()
var stateUpdatesPublisher: AnyPublisher<Void, Never> {
stateUpdatesSubject.eraseToAnyPublisher()
private let actionsSubject = PassthroughSubject<RoomProxyAction, Never>()
var actions: AnyPublisher<RoomProxyAction, Never> {
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)
})
}
}
@@ -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<Void, Never> { get }
var actions: AnyPublisher<RoomProxyAction, Never> { get }
var timeline: TimelineProxyProtocol { get }
@@ -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<BackPaginationStatus, Never>()
private let timelineUpdatesSubject = PassthroughSubject<[TimelineDiff], Never>()
private let actionsSubject = PassthroughSubject<TimelineProxyAction, Never>()
var actions: AnyPublisher<TimelineProxyAction, Never> {
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<Void, TimelineProxyError> {
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)
@@ -33,9 +33,16 @@ enum TimelineProxyError: Error, Equatable {
case failedSendingPollResponse
}
enum TimelineProxyAction {
case sentMessage
}
// sourcery: AutoMockable
protocol TimelineProxyProtocol {
var actions: AnyPublisher<TimelineProxyAction, Never> { get }
var timelineProvider: RoomTimelineProviderProtocol { get }
func subscribeForUpdates() async
/// Cancels a failed message given its transaction ID from the timeline
@@ -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