Mark rooms as read on push and pop. (#462)

We don't have visibility into timeline item read status so can't do this continuously yet.

* Simplify RoomTimelineProvider's responsibilities.
This commit is contained in:
Doug
2023-01-18 15:14:03 +00:00
committed by GitHub
parent 6615116226
commit 5017ba7122
14 changed files with 183 additions and 226 deletions
@@ -64,6 +64,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
} }
func stop() { func stop() {
viewModel?.context.send(viewAction: .markRoomAsRead)
viewModel?.stop() viewModel?.stop()
viewModel = nil viewModel = nil
parameters = nil parameters = nil
@@ -106,7 +107,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
MXLog.debug("Selected \(emoji) for \(itemId)") MXLog.debug("Selected \(emoji) for \(itemId)")
self?.navigationStackCoordinator.setSheetCoordinator(nil) self?.navigationStackCoordinator.setSheetCoordinator(nil)
Task { Task {
await timelineController.sendReaction(emoji, for: itemId) await timelineController.sendReaction(emoji, to: itemId)
} }
case .dismiss: case .dismiss:
self?.navigationStackCoordinator.setSheetCoordinator(nil) self?.navigationStackCoordinator.setSheetCoordinator(nil)
@@ -42,6 +42,8 @@ enum RoomScreenViewAction {
case sendReaction(key: String, eventID: String) case sendReaction(key: String, eventID: String)
case cancelReply case cancelReply
case cancelEdit case cancelEdit
/// Mark the entire room as read - this is heavy handed as a starting point for now.
case markRoomAsRead
} }
struct RoomScreenViewState: BindableState { struct RoomScreenViewState: BindableState {
@@ -41,7 +41,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
self.timelineViewFactory = timelineViewFactory self.timelineViewFactory = timelineViewFactory
self.mediaProvider = mediaProvider self.mediaProvider = mediaProvider
super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomId, super.init(initialViewState: RoomScreenViewState(roomId: timelineController.roomID,
roomTitle: roomName ?? "Unknown room 💥", roomTitle: roomName ?? "Unknown room 💥",
roomAvatar: nil, roomAvatar: nil,
bindings: .init(composerText: "", composerFocused: false))) bindings: .init(composerText: "", composerFocused: false)))
@@ -109,13 +109,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .sendMessage: case .sendMessage:
await sendCurrentMessage() await sendCurrentMessage()
case .sendReaction(let emoji, let itemId): case .sendReaction(let emoji, let itemId):
await timelineController.sendReaction(emoji, for: itemId) await timelineController.sendReaction(emoji, to: itemId)
case .displayEmojiPicker(let itemId): case .displayEmojiPicker(let itemId):
callback?(.displayEmojiPicker(itemId: itemId)) callback?(.displayEmojiPicker(itemId: itemId))
case .cancelReply: case .cancelReply:
state.composerMode = .default state.composerMode = .default
case .cancelEdit: case .cancelEdit:
state.composerMode = .default state.composerMode = .default
state.bindings.composerText = ""
case .markRoomAsRead:
await markRoomAsRead()
} }
} }
@@ -134,6 +137,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
break break
} }
} }
private func markRoomAsRead() async {
_ = await timelineController.markRoomAsRead()
}
private func itemTapped(with itemId: String) async { private func itemTapped(with itemId: String) async {
state.showLoading = true state.showLoading = true
@@ -171,9 +178,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
switch currentComposerState { switch currentComposerState {
case .reply(let itemId, _): case .reply(let itemId, _):
await timelineController.sendReply(currentMessage, to: itemId) await timelineController.sendMessage(currentMessage, inReplyTo: itemId)
case .edit(let originalItemId): case .edit(let originalItemId):
await timelineController.editMessage(currentMessage, of: originalItemId) await timelineController.editMessage(currentMessage, original: originalItemId)
default: default:
await timelineController.sendMessage(currentMessage) await timelineController.sendMessage(currentMessage)
} }
@@ -250,7 +257,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.composerText = "> \(item.text)" state.bindings.composerText = "> \(item.text)"
case .copyPermalink: case .copyPermalink:
do { do {
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomId) let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: item.id, roomIdentifier: timelineController.roomID)
UIPasteboard.general.url = permalink UIPasteboard.general.url = permalink
} catch { } catch {
displayError(.alert(ElementL10n.roomTimelinePermalinkCreationFailure)) displayError(.alert(ElementL10n.roomTimelinePermalinkCreationFailure))
@@ -263,12 +270,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.composerFocused = true state.bindings.composerFocused = true
state.composerMode = .reply(id: item.id, displayName: item.sender.displayName ?? item.sender.id) state.composerMode = .reply(id: item.id, displayName: item.sender.displayName ?? item.sender.id)
case .viewSource: case .viewSource:
let debugDescription = timelineController.debugDescriptionFor(item.id) let debugDescription = timelineController.debugDescription(for: item.id)
MXLog.info(debugDescription) MXLog.info(debugDescription)
state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription) state.bindings.debugInfo = .init(title: "Timeline item", content: debugDescription)
case .retryDecryption(let sessionId): case .retryDecryption(let sessionId):
Task { Task {
await timelineController.retryDecryption(forSessionId: sessionId) await timelineController.retryDecryption(for: sessionId)
} }
} }
@@ -29,6 +29,13 @@ struct RoomScreen: View {
.overlay { loadingIndicator } .overlay { loadingIndicator }
.alert(item: $context.alertInfo) { $0.alert } .alert(item: $context.alertInfo) { $0.alert }
.sheet(item: $context.debugInfo) { DebugScreen(info: $0) } .sheet(item: $context.debugInfo) { DebugScreen(info: $0) }
.task {
// Give a couple of seconds for items to load and to see them.
try? await Task.sleep(for: .seconds(2))
guard !Task.isCancelled else { return }
context.send(viewAction: .markRoomAsRead)
}
} }
var timeline: some View { var timeline: some View {
@@ -29,6 +29,7 @@ struct MockRoomProxy: RoomProxyProtocol {
var isPublic = Bool.random() var isPublic = Bool.random()
var isEncrypted = Bool.random() var isEncrypted = Bool.random()
var isTombstoned = Bool.random() var isTombstoned = Bool.random()
var hasUnreadNotifications = Bool.random()
var members: [RoomMemberProxy]? var members: [RoomMemberProxy]?
let timelineProvider: RoomTimelineProviderProtocol = MockRoomTimelineProvider() let timelineProvider: RoomTimelineProviderProtocol = MockRoomTimelineProvider()
@@ -58,16 +59,20 @@ struct MockRoomProxy: RoomProxyProtocol {
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> { func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> {
.failure(.failedPaginatingBackwards) .failure(.failedPaginatingBackwards)
} }
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError> {
.failure(.failedSendingReadReceipt)
}
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> { func sendMessage(_ message: String, inReplyTo eventID: String? = nil) async -> Result<Void, RoomProxyError> {
.failure(.failedSendingMessage) .failure(.failedSendingMessage)
} }
func sendReaction(_ reaction: String, for eventId: String) async -> Result<Void, RoomProxyError> { func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError> {
.failure(.failedSendingMessage) .failure(.failedSendingMessage)
} }
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError> { func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
.failure(.failedSendingMessage) .failure(.failedSendingMessage)
} }
@@ -82,5 +87,5 @@ struct MockRoomProxy: RoomProxyProtocol {
return .failure(.failedRetrievingMembers) return .failure(.failedRetrievingMembers)
} }
func retryDecryption(forSessionId sessionId: String) async { } func retryDecryption(for sessionID: String) async { }
} }
+43 -18
View File
@@ -24,6 +24,7 @@ class RoomProxy: RoomProxyProtocol {
private let slidingSyncRoom: SlidingSyncRoomProtocol private let slidingSyncRoom: SlidingSyncRoomProtocol
private let room: RoomProtocol private let room: RoomProtocol
private let backgroundTaskService: BackgroundTaskServiceProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol
private let backgroundTaskName = "SendRoomEvent"
private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.serial") private let serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomproxy.serial")
@@ -82,6 +83,10 @@ class RoomProxy: RoomProxyProtocol {
room.isTombstoned() room.isTombstoned()
} }
var hasUnreadNotifications: Bool {
slidingSyncRoom.hasUnreadNotifications()
}
var avatarURL: URL? { var avatarURL: URL? {
room.avatarUrl().flatMap(URL.init(string:)) room.avatarUrl().flatMap(URL.init(string:))
} }
@@ -156,18 +161,34 @@ class RoomProxy: RoomProxyProtocol {
} }
} }
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> { func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
return await Task.dispatch(on: serialDispatchQueue) {
do {
try self.room.sendReadReceipt(eventId: eventID)
return .success(())
} catch {
return .failure(.failedSendingReadReceipt)
}
}
}
func sendMessage(_ message: String, inReplyTo eventID: String? = nil) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer { defer {
sendMessageBackgroundTask?.stop() sendMessageBackgroundTask?.stop()
} }
let transactionId = genTransactionId() let transactionId = genTransactionId()
return await Task.dispatch(on: .global()) { return await Task.dispatch(on: serialDispatchQueue) {
do { do {
if let inReplyToEventId { if let eventID {
try self.room.sendReply(msg: message, inReplyToEventId: inReplyToEventId, txnId: transactionId) try self.room.sendReply(msg: message, inReplyToEventId: eventID, txnId: transactionId)
} else { } else {
let messageContent = messageEventContentFromMarkdown(md: message) let messageContent = messageEventContentFromMarkdown(md: message)
try self.room.send(msg: messageContent, txnId: transactionId) try self.room.send(msg: messageContent, txnId: transactionId)
@@ -179,15 +200,15 @@ class RoomProxy: RoomProxyProtocol {
} }
} }
func sendReaction(_ reaction: String, for eventId: String) async -> Result<Void, RoomProxyError> { func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer { defer {
sendMessageBackgroundTask?.stop() sendMessageBackgroundTask?.stop()
} }
return await Task.dispatch(on: .global()) { return await Task.dispatch(on: serialDispatchQueue) {
do { do {
try self.room.sendReaction(eventId: eventId, key: reaction) try self.room.sendReaction(eventId: eventID, key: reaction)
return .success(()) return .success(())
} catch { } catch {
return .failure(.failedSendingReaction) return .failure(.failedSendingReaction)
@@ -195,17 +216,17 @@ class RoomProxy: RoomProxyProtocol {
} }
} }
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError> { func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true) sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer { defer {
sendMessageBackgroundTask?.stop() sendMessageBackgroundTask?.stop()
} }
let transactionId = genTransactionId() let transactionId = genTransactionId()
return await Task.dispatch(on: .global()) { return await Task.dispatch(on: serialDispatchQueue) {
do { do {
try self.room.edit(newMsg: newMessage, originalEventId: originalEventId, txnId: transactionId) try self.room.edit(newMsg: newMessage, originalEventId: eventID, txnId: transactionId)
return .success(()) return .success(())
} catch { } catch {
return .failure(.failedEditingMessage) return .failure(.failedEditingMessage)
@@ -214,17 +235,21 @@ class RoomProxy: RoomProxyProtocol {
} }
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> { func redact(_ eventID: String) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
let transactionID = genTransactionId() let transactionID = genTransactionId()
return await Task { return await Task.dispatch(on: serialDispatchQueue) {
do { do {
try room.redact(eventId: eventID, reason: nil, txnId: transactionID) try self.room.redact(eventId: eventID, reason: nil, txnId: transactionID)
return .success(()) return .success(())
} catch { } catch {
return .failure(.failedRedactingEvent) return .failure(.failedRedactingEvent)
} }
} }
.value
} }
func members() async -> Result<[RoomMemberProxy], RoomProxyError> { func members() async -> Result<[RoomMemberProxy], RoomProxyError> {
@@ -238,9 +263,9 @@ class RoomProxy: RoomProxyProtocol {
} }
} }
func retryDecryption(forSessionId sessionId: String) async { func retryDecryption(for sessionID: String) async {
await Task.dispatch(on: .global()) { [weak self] in await Task.dispatch(on: .global()) { [weak self] in
self?.room.retryDecryption(sessionIds: [sessionId]) self?.room.retryDecryption(sessionIds: [sessionID])
} }
} }
@@ -23,6 +23,7 @@ enum RoomProxyError: Error {
case failedPaginatingBackwards case failedPaginatingBackwards
case failedRetrievingMemberAvatarURL case failedRetrievingMemberAvatarURL
case failedRetrievingMemberDisplayName case failedRetrievingMemberDisplayName
case failedSendingReadReceipt
case failedSendingMessage case failedSendingMessage
case failedSendingReaction case failedSendingReaction
case failedEditingMessage case failedEditingMessage
@@ -39,6 +40,7 @@ protocol RoomProxyProtocol {
var isSpace: Bool { get } var isSpace: Bool { get }
var isEncrypted: Bool { get } var isEncrypted: Bool { get }
var isTombstoned: Bool { get } var isTombstoned: Bool { get }
var hasUnreadNotifications: Bool { get }
var name: String? { get } var name: String? { get }
var displayName: String? { get } var displayName: String? { get }
@@ -59,21 +61,23 @@ protocol RoomProxyProtocol {
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError>
func sendMessage(_ message: String, inReplyToEventId: String?) async -> Result<Void, RoomProxyError> func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError>
func sendReaction(_ reaction: String, for eventId: String) async -> Result<Void, RoomProxyError> func sendMessage(_ message: String, inReplyTo eventID: String?) async -> Result<Void, RoomProxyError>
func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError>
func editMessage(_ newMessage: String, originalEventId: String) async -> Result<Void, RoomProxyError> func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError>
func redact(_ eventID: String) async -> Result<Void, RoomProxyError> func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
func members() async -> Result<[RoomMemberProxy], RoomProxyError> func members() async -> Result<[RoomMemberProxy], RoomProxyError>
func retryDecryption(forSessionId sessionId: String) async func retryDecryption(for sessionID: String) async
} }
extension RoomProxyProtocol { extension RoomProxyProtocol {
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> { func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
await sendMessage(message, inReplyToEventId: nil) await sendMessage(message, inReplyTo: nil)
} }
} }
@@ -20,26 +20,4 @@ struct MockRoomTimelineProvider: RoomTimelineProviderProtocol {
var itemsPublisher = CurrentValueSubject<[TimelineItemProxy], Never>([]) var itemsPublisher = CurrentValueSubject<[TimelineItemProxy], Never>([])
private var itemProxies = [TimelineItemProxy]() private var itemProxies = [TimelineItemProxy]()
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedPaginatingBackwards)
}
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedSendingMessage)
}
func sendReaction(_ reaction: String, for itemId: String) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedSendingReaction)
}
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedSendingMessage)
}
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
.failure(.failedRedactingItem)
}
func retryDecryption(forSessionId sessionId: String) async { }
} }
@@ -61,79 +61,6 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
} }
} }
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineProviderError> {
MXLog.info("Started back pagination request")
switch await roomProxy.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) {
case .success:
MXLog.info("Finished back pagination request")
return .success(())
case .failure(let error):
MXLog.error("Failed back pagination request with error: \(error)")
if error == .noMoreMessagesToBackPaginate {
return .failure(.noMoreMessagesToBackPaginate)
}
return .failure(.failedPaginatingBackwards)
}
}
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError> {
if let inReplyToItemId {
MXLog.info("Sending message in reply to: \(inReplyToItemId)")
} else {
MXLog.info("Sending message")
}
switch await roomProxy.sendMessage(message, inReplyToEventId: inReplyToItemId) {
case .success:
MXLog.info("Finished sending message")
return .success(())
case .failure(let error):
MXLog.error("Failed sending message with error: \(error)")
return .failure(.failedSendingMessage)
}
}
func sendReaction(_ reaction: String, for itemId: String) async -> Result<Void, RoomTimelineProviderError> {
switch await roomProxy.sendReaction(reaction, for: itemId) {
case .success:
MXLog.info("Finished sending reaction")
return .success(())
case .failure(let error):
MXLog.error("Failed sending reaction with error: \(error)")
return .failure(.failedSendingReaction)
}
}
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError> {
MXLog.info("Editing message: \(originalItemId)")
switch await roomProxy.editMessage(newMessage, originalEventId: originalItemId) {
case .success:
MXLog.info("Finished editing message")
return .success(())
case .failure(let error):
MXLog.error("Failed editing message with error: \(error)")
return .failure(.failedSendingMessage)
}
}
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError> {
MXLog.info("Redacting message: \(eventID)")
switch await roomProxy.redact(eventID) {
case .success:
MXLog.info("Finished redacting message")
return .success(())
case .failure(let error):
MXLog.error("Failed redacting message with error: \(error)")
return .failure(.failedRedactingItem)
}
}
func retryDecryption(forSessionId sessionId: String) async {
await roomProxy.retryDecryption(forSessionId: sessionId)
}
// MARK: - Private // MARK: - Private
private func updateItemsWithDiff(_ diff: TimelineDiff) { private func updateItemsWithDiff(_ diff: TimelineDiff) {
@@ -17,33 +17,6 @@
import Combine import Combine
import Foundation import Foundation
enum RoomTimelineProviderError: Error {
case noMoreMessagesToBackPaginate
case failedPaginatingBackwards
case failedSendingMessage
case failedSendingReaction
case failedRedactingItem
case generic
}
protocol RoomTimelineProviderProtocol { protocol RoomTimelineProviderProtocol {
var itemsPublisher: CurrentValueSubject<[TimelineItemProxy], Never> { get } var itemsPublisher: CurrentValueSubject<[TimelineItemProxy], Never> { get }
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineProviderError>
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError>
func sendReaction(_ reaction: String, for itemId: String) async -> Result<Void, RoomTimelineProviderError>
func editMessage(_ newMessage: String, originalItemId: String) async -> Result<Void, RoomTimelineProviderError>
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError>
func retryDecryption(forSessionId sessionId: String) async
}
extension RoomTimelineProviderProtocol {
func sendMessage(_ message: String) async -> Result<Void, RoomTimelineProviderError> {
await sendMessage(message, inReplyToItemId: nil)
}
} }
@@ -26,7 +26,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// An array of timeline items that will be appended in order when ``simulateIncomingItems()`` is called. /// An array of timeline items that will be appended in order when ``simulateIncomingItems()`` is called.
var incomingItems: [RoomTimelineItemProtocol] = [] var incomingItems: [RoomTimelineItemProtocol] = []
let roomId = "MockRoomIdentifier" let roomID = "MockRoomIdentifier"
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>() let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
@@ -46,27 +46,27 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
return .success(()) return .success(())
} }
func processItemAppearance(_ itemId: String) async { } func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> { .success(()) }
func processItemDisappearance(_ itemId: String) async { } func processItemAppearance(_ itemID: String) async { }
func processItemDisappearance(_ itemID: String) async { }
func processItemTap(_ itemId: String) async -> RoomTimelineControllerAction { .none } func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction { .none }
func sendMessage(_ message: String) async { } func sendMessage(_ message: String, inReplyTo itemID: String?) async { }
func sendReply(_ message: String, to itemId: String) async { } func sendReaction(_ reaction: String, to itemID: String) async { }
func sendReaction(_ reaction: String, for itemId: String) async { }
func editMessage(_ newMessage: String, of itemId: String) async { } func editMessage(_ newMessage: String, original itemID: String) async { }
func redact(_ eventID: String) async { } func redact(_ itemID: String) async { }
func debugDescriptionFor(_ itemId: String) -> String { func debugDescription(for itemID: String) -> String {
"Mock debug description" "Mock debug description"
} }
func retryDecryption(forSessionId sessionId: String) async { } func retryDecryption(for sessionID: String) async { }
// MARK: - UI Test signalling // MARK: - UI Test signalling
@@ -37,7 +37,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
private(set) var timelineItems = [RoomTimelineItemProtocol]() private(set) var timelineItems = [RoomTimelineItemProtocol]()
var roomId: String { var roomID: String {
roomProxy.id roomProxy.id
} }
@@ -68,19 +68,35 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> { func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> {
switch await timelineProvider.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) { MXLog.info("Started back pagination request")
switch await roomProxy.paginateBackwards(requestSize: requestSize, untilNumberOfItems: untilNumberOfItems) {
case .success: 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(()) return .success(())
case .failure(let error): case .failure(let error):
if error == .noMoreMessagesToBackPaginate { MXLog.error("Failed back pagination request with error: \(error)")
return .success(())
}
return .failure(.generic) return .failure(.generic)
} }
} }
func processItemAppearance(_ itemId: String) async { func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError> {
guard let timelineItem = timelineItems.first(where: { $0.id == itemId }) else { guard roomProxy.hasUnreadNotifications,
let eventID = timelineItems.last?.id
else { return .success(()) }
switch await roomProxy.sendReadReceipt(for: eventID) {
case .success:
return .success(())
case .failure:
return .failure(.generic)
}
}
func processItemAppearance(_ itemID: String) async {
guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return return
} }
@@ -101,18 +117,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
} }
func processItemDisappearance(_ itemId: String) { } func processItemDisappearance(_ itemID: String) { }
// swiftlint:disable:next cyclomatic_complexity // swiftlint:disable:next cyclomatic_complexity
func processItemTap(_ itemId: String) async -> RoomTimelineControllerAction { func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction {
guard let timelineItem = timelineItems.first(where: { $0.id == itemId }) else { guard let timelineItem = timelineItems.first(where: { $0.id == itemID }) else {
return .none return .none
} }
switch timelineItem { switch timelineItem {
case let item as ImageRoomTimelineItem: case let item as ImageRoomTimelineItem:
await loadFileForImageTimelineItem(item) await loadFileForImageTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemId }), guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? ImageRoomTimelineItem else { let item = timelineItems[index] as? ImageRoomTimelineItem else {
return .none return .none
} }
@@ -122,7 +138,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none return .none
case let item as VideoRoomTimelineItem: case let item as VideoRoomTimelineItem:
await loadVideoForTimelineItem(item) await loadVideoForTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemId }), guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? VideoRoomTimelineItem else { let item = timelineItems[index] as? VideoRoomTimelineItem else {
return .none return .none
} }
@@ -132,7 +148,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return .none return .none
case let item as FileRoomTimelineItem: case let item as FileRoomTimelineItem:
await loadFileForTimelineItem(item) await loadFileForTimelineItem(item)
guard let index = timelineItems.firstIndex(where: { $0.id == itemId }), guard let index = timelineItems.firstIndex(where: { $0.id == itemID }),
let item = timelineItems[index] as? FileRoomTimelineItem else { let item = timelineItems[index] as? FileRoomTimelineItem else {
return .none return .none
} }
@@ -145,54 +161,59 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
} }
func sendMessage(_ message: String) async { func sendMessage(_ message: String, inReplyTo itemID: String?) async {
MXLog.info("Send message in \(roomId)") if itemID == nil {
switch await timelineProvider.sendMessage(message) { MXLog.info("Send message in \(roomID)")
default: } else {
break MXLog.info("Send reply in \(roomID)")
}
switch await roomProxy.sendMessage(message, inReplyTo: itemID) {
case .success:
MXLog.info("Finished sending message")
case .failure(let error):
MXLog.error("Failed sending message with error: \(error)")
} }
} }
func sendReply(_ message: String, to itemId: String) async { func sendReaction(_ reaction: String, to itemID: String) async {
MXLog.info("Send reply in \(roomId)") MXLog.info("Send reaction in \(roomID)")
switch await timelineProvider.sendMessage(message, inReplyToItemId: itemId) { switch await roomProxy.sendReaction(reaction, to: itemID) {
default: case .success:
break MXLog.info("Finished sending reaction")
case .failure(let error):
MXLog.error("Failed sending reaction with error: \(error)")
} }
} }
func sendReaction(_ reaction: String, for itemId: String) async { func editMessage(_ newMessage: String, original itemID: String) async {
MXLog.info("Send reaction in \(roomId)") MXLog.info("Edit message in \(roomID)")
switch await timelineProvider.sendReaction(reaction, for: itemId) { switch await roomProxy.editMessage(newMessage, original: itemID) {
default: case .success:
break MXLog.info("Finished editing message")
} case .failure(let error):
} MXLog.error("Failed editing message with error: \(error)")
func editMessage(_ newMessage: String, of itemId: String) async {
MXLog.info("Edit message in \(roomId)")
switch await timelineProvider.editMessage(newMessage, originalItemId: itemId) {
default:
break
} }
} }
func redact(_ eventID: String) async { func redact(_ itemID: String) async {
MXLog.info("Send redaction in \(roomId)") MXLog.info("Send redaction in \(roomID)")
switch await timelineProvider.redact(eventID) { switch await roomProxy.redact(itemID) {
default: case .success:
break MXLog.info("Finished redacting message")
case .failure(let error):
MXLog.error("Failed redacting message with error: \(error)")
} }
} }
// Handle this paralel to the timeline items so we're not forced // Handle this parallel to the timeline items so we're not forced
// to bundle the Rust side objects within them // to bundle the Rust side objects within them
func debugDescriptionFor(_ itemId: String) -> String { func debugDescription(for itemID: String) -> String {
var description = "Unknown item" var description = "Unknown item"
timelineProvider.itemsPublisher.value.forEach { timelineItemProxy in timelineProvider.itemsPublisher.value.forEach { timelineItemProxy in
switch timelineItemProxy { switch timelineItemProxy {
case .event(let item): case .event(let item):
if item.id == itemId { if item.id == itemID {
description = item.debugDescription description = item.debugDescription
return return
} }
@@ -204,8 +225,8 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return description return description
} }
func retryDecryption(forSessionId sessionId: String) async { func retryDecryption(for sessionID: String) async {
await timelineProvider.retryDecryption(forSessionId: sessionId) await roomProxy.retryDecryption(for: sessionID)
} }
// MARK: - Private // MARK: - Private
@@ -37,30 +37,36 @@ enum RoomTimelineControllerError: Error {
@MainActor @MainActor
protocol RoomTimelineControllerProtocol { protocol RoomTimelineControllerProtocol {
var roomId: String { get } var roomID: String { get }
var timelineItems: [RoomTimelineItemProtocol] { get } var timelineItems: [RoomTimelineItemProtocol] { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get } var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
func processItemAppearance(_ itemId: String) async func processItemAppearance(_ itemID: String) async
func processItemDisappearance(_ itemId: String) async func processItemDisappearance(_ itemID: String) async
func processItemTap(_ itemId: String) async -> RoomTimelineControllerAction func processItemTap(_ itemID: String) async -> RoomTimelineControllerAction
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError> func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomTimelineControllerError>
func sendMessage(_ message: String) async func markRoomAsRead() async -> Result<Void, RoomTimelineControllerError>
func sendReply(_ message: String, to itemId: String) async func sendMessage(_ message: String, inReplyTo itemID: String?) async
func editMessage(_ newMessage: String, of itemId: String) async func editMessage(_ newMessage: String, original itemID: String) async
func sendReaction(_ reaction: String, for itemId: String) async func sendReaction(_ reaction: String, to itemID: String) async
func redact(_ eventID: String) async func redact(_ itemID: String) async
func debugDescriptionFor(_ itemId: String) -> String func debugDescription(for itemID: String) -> String
func retryDecryption(forSessionId sessionId: String) async func retryDecryption(for sessionID: String) async
}
extension RoomTimelineControllerProtocol {
func sendMessage(_ message: String) async {
await sendMessage(message, inReplyTo: nil)
}
} }
+1
View File
@@ -0,0 +1 @@
Rooms: Mark rooms as read when opening/closing.