mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
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:
@@ -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 { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -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
|
||||||
|
|
||||||
|
|||||||
+67
-46
@@ -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
|
||||||
|
|||||||
+17
-11
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Rooms: Mark rooms as read when opening/closing.
|
||||||
Reference in New Issue
Block a user