mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Fixes #2147 - Scroll the timeline to the bottom when sending message as well as media, polls and locations.
This commit is contained in:
committed by
Stefan Ceriu
parent
6905ac5f70
commit
8189b0a471
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user