Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public enum WireMessagingAssembly {
@MainActor
public static func makeConversationScreen(
loadMessagesRepo: any (LoadConversationMessagesRepositoryProtocol & MonitorMessagesRepositoryProtocol),
senderNameObserverProvider: SenderNameObserverProvider?,
reactionsObserverProvider: ReactionsObserverProvider?
senderNameObserverProvider: @escaping SenderNameObserverProvider,
reactionsObserverProvider: @escaping ReactionsObserverProvider
) -> UIViewController {
ConversationMessagesViewController(
viewModel: ConversationMessagesViewModel(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,38 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
public import Foundation

public protocol LoadConversationMessagesRepositoryProtocol: Sendable {
func loadMessages(offset: Int, limit: Int) async -> [MessageModel]
var hasOlderMessagesToLoad: Bool { get }
func loadMessages(offset: Int) async -> [MessageModel]
func loadOlderMessages(lastMessageTimestamp: Date) async -> [MessageModel]
}

private let kLoadMessagesDefaultBatchSize = 30
public let kLoadMessagesDefaultBatchSize = 30

package protocol LoadConversationMessagesUseCaseProtocol: Sendable {
var hasOlderMessagesToLoad: Bool { get }
func loadMessages(offset: Int) async -> [MessageModel]
func loadOlderMessages(lastMessageTimestamp: Date) async -> [MessageModel]
}

package final class LoadConversationMessagesUseCase: LoadConversationMessagesUseCaseProtocol {

private let repo: any LoadConversationMessagesRepositoryProtocol

package var hasOlderMessagesToLoad: Bool { repo.hasOlderMessagesToLoad }

package init(repo: any LoadConversationMessagesRepositoryProtocol) {
self.repo = repo
}

package func loadMessages(offset: Int) async -> [MessageModel] {
await repo.loadMessages(offset: offset, limit: kLoadMessagesDefaultBatchSize)
await repo.loadMessages(offset: offset)
}

package func loadOlderMessages(lastMessageTimestamp: Date) async -> [MessageModel] {
await repo.loadOlderMessages(lastMessageTimestamp: lastMessageTimestamp)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

public import Foundation

public typealias ReactionsModel = [String: [UserModel]]

public struct MessageModel: Sendable {
Expand All @@ -27,17 +29,20 @@ public struct MessageModel: Sendable {
}

public let objectID: any Sendable
public let serverTimestamp: Date?
public let sender: UserModel?
public let kind: Kind
public let reactions: ReactionsModel

public init(
objectID: any Sendable,
serverTimestamp: Date?,
sender: UserModel?,
kind: Kind,
reactions: ReactionsModel
) {
self.objectID = objectID
self.serverTimestamp = serverTimestamp
self.sender = sender
self.kind = kind
self.reactions = reactions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ public protocol ReactionsObserverProtocol {
var reactionsPublisher: AnyPublisher<ReactionsModel, Never>? { get }
}

public typealias ReactionsObserverProvider = (MessageModel) -> (any ReactionsObserverProtocol)?
public typealias ReactionsObserverProvider = (MessageModel) -> any ReactionsObserverProtocol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ public protocol SenderNameObserverProtocol {
var authorChangedPublisher: AnyPublisher<String, Never>? { get }
}

public typealias SenderNameObserverProvider = (UserModel?) -> (any SenderNameObserverProtocol)?
public typealias SenderNameObserverProvider = (UserModel) -> any SenderNameObserverProtocol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

public import WireMessagingDomain

import UIKit
package import UIKit

public class MockChannelHistoryUseCaseProtocol: ChannelHistoryUseCaseProtocol {

Expand Down Expand Up @@ -188,6 +188,16 @@ package class MockLoadConversationMessagesUseCaseProtocol: LoadConversationMessa

package init() { }

// MARK: - hasOlderMessagesToLoad

package var hasOlderMessagesToLoad: Bool {
get { return underlyingHasOlderMessagesToLoad }
set(value) { underlyingHasOlderMessagesToLoad = value }
}

package var underlyingHasOlderMessagesToLoad: Bool!


// MARK: - loadMessages

package var loadMessagesOffset_Invocations: [Int] = []
Expand All @@ -206,6 +216,24 @@ package class MockLoadConversationMessagesUseCaseProtocol: LoadConversationMessa
}
}

// MARK: - loadOlderMessages

package var loadOlderMessagesLastMessageTimestamp_Invocations: [Date] = []
package var loadOlderMessagesLastMessageTimestamp_MockMethod: ((Date) async -> [MessageModel])?
package var loadOlderMessagesLastMessageTimestamp_MockValue: [MessageModel]?

package func loadOlderMessages(lastMessageTimestamp: Date) async -> [MessageModel] {
loadOlderMessagesLastMessageTimestamp_Invocations.append(lastMessageTimestamp)

if let mock = loadOlderMessagesLastMessageTimestamp_MockMethod {
return await mock(lastMessageTimestamp)
} else if let mock = loadOlderMessagesLastMessageTimestamp_MockValue {
return mock
} else {
fatalError("no mock for `loadOlderMessagesLastMessageTimestamp`")
}
}

}

package class MockMonitorMessagesUseCaseProtocol: MonitorMessagesUseCaseProtocol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@ package import WireMessagingDomain
// to UI model which is Attributed string
package struct AnyObserverProvider: @unchecked Sendable {

package let senderNameObserverProvider: SenderNameObserverProvider?
package let reactionsObserverProvider: ReactionsObserverProvider?
package let senderNameObserverProvider: SenderNameObserverProvider
package let reactionsObserverProvider: ReactionsObserverProvider

package init(
senderNameObserverProvider: SenderNameObserverProvider?,
reactionsObserverProvider: ReactionsObserverProvider?
senderNameObserverProvider: @escaping SenderNameObserverProvider,
reactionsObserverProvider: @escaping ReactionsObserverProvider
) {
self.senderNameObserverProvider = senderNameObserverProvider
self.reactionsObserverProvider = reactionsObserverProvider
}

func get(for message: MessageModel) -> AnyPublisher<ReactionsModel, Never>? {
reactionsObserverProvider?(message)?.reactionsPublisher
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Combine
import Foundation
package import UIKit
package import WireMessagingDomain
package import WireLogging

package enum ConversationSection: Sendable {
// one section for now, later we'd have probably one section for a day
Expand All @@ -32,6 +33,8 @@ package protocol ConversationDataSourceProtocol: Sendable {
func makeUpdatesStream() async -> AsyncStream<MessagesUpdate>
func loadInitialMessages() async
func reset() async
func loadOlderMessages() async
func loadNewerMessages() async
}

/// Actor to synchronise access to all that needed to conversation screen
Expand Down Expand Up @@ -108,18 +111,23 @@ package actor ConversationDataSource: @preconcurrency ConversationDataSourceProt
} else {
.empty
}

var senderNamePublisher: AnyPublisher<String, Never>?
if let sender = model.sender {
senderNamePublisher = observersProvider.senderNameObserverProvider(sender).authorChangedPublisher
}

return ConversationElement.text(
TextMessageViewModel(
content: AttributedString(stringLiteral: textModel.text ?? ""),
serverTimestamp: model.serverTimestamp,
senderViewModel: SenderViewModel(
state: senderState,
namePublisher: observersProvider
.senderNameObserverProvider?(model.sender)?.authorChangedPublisher
namePublisher: senderNamePublisher
),
reactionsViewModel: ReactionsViewModel(
state: ReactionsViewModel.state(from: model.reactions),
publisher: observersProvider.get(for: model)
publisher: observersProvider.reactionsObserverProvider(model).reactionsPublisher
)
)
)
Expand All @@ -143,10 +151,51 @@ package actor ConversationDataSource: @preconcurrency ConversationDataSourceProt
}

// load older messages
func loadOlderMessages() {}

var loadingMessages = false
var currentOffset = 0
package func loadOlderMessages() async {
guard !loadingMessages, loadMessagesUseCase.hasOlderMessagesToLoad else {
print("DS: loadOlderMessages: guard already loading")
updatesStreamContinuation?.yield(.noMoreMessagesToLoad)
return
}

// NOTE: we dispatch async because `didScroll(tableView:)` can be called inside a `performBatchUpdate()`,
// which would cause data source inconsistency if change the fetchLimit.

guard let oldestMessageTimestamp = snapshot.itemIdentifiers.first(where: { $0.serverTimestamp != nil })?.serverTimestamp else {
print("DS: loadOlderMessages: can't find oldest message")
updatesStreamContinuation?.yield(.noMoreMessagesToLoad)
return
}

loadingMessages = true
let messages = await loadMessagesUseCase.loadOlderMessages(
lastMessageTimestamp: oldestMessageTimestamp
)
guard messages.count > 0, let beforeItem = snapshot.itemIdentifiers.first else {
WireLogger.conversation.error(
"Failed to get beforeItem for snapshot.itemIdentifiers.first to insert new loaded messages")
updatesStreamContinuation?.yield(.noMoreMessagesToLoad)
return
}

snapshot.insertItems(
messages
.reversed()
.map { mapToUIModel($0) },
beforeItem: beforeItem
)
updatesStreamContinuation?.yield(.loadedOlderMessages(snapshot))

loadingMessages = false
}

// load newer messages
func loadNewerMessages() {}
package func loadNewerMessages() {

}

// MARK: - private

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,17 @@
// along with this program. If not, see http://www.gnu.org/licenses/.
//

import Foundation
package import Foundation

package enum ConversationElement: Hashable, Sendable {

case text(TextMessageViewModel)
// case image, video, system, etc

package var serverTimestamp: Date? {
switch self {
case .text(let textMessageViewModel):
return textMessageViewModel.serverTimestamp
}
}
}
Loading