Files
mobile-ios/Rosetta/Features/Chats/ChatDetail/ChatDetailViewModel.swift

66 lines
2.3 KiB
Swift

import Foundation
import Combine
/// Per-dialog observation isolation for ChatDetailView.
///
/// Instead of `@ObservedObject messageRepository` (which re-renders on ANY dialog change),
/// this ViewModel subscribes only to the specific dialog's messages via Combine pipeline
/// with `removeDuplicates()`. The view re-renders ONLY when its own dialog's data changes.
@MainActor
final class ChatDetailViewModel: ObservableObject {
let dialogKey: String
@Published private(set) var messages: [ChatMessage] = []
@Published private(set) var isTyping: Bool = false
private var cancellables = Set<AnyCancellable>()
init(dialogKey: String) {
self.dialogKey = dialogKey
let repo = MessageRepository.shared
// Seed with current values
messages = repo.messages(for: dialogKey)
isTyping = repo.isTyping(dialogKey: dialogKey)
// Subscribe to messagesByDialog changes, filtered to our dialog only.
// Broken into steps to help the Swift type-checker.
let key = dialogKey
let messagesPublisher = repo.$messagesByDialog
.map { (dict: [String: [ChatMessage]]) -> [ChatMessage] in
dict[key] ?? []
}
.removeDuplicates { (lhs: [ChatMessage], rhs: [ChatMessage]) -> Bool in
guard lhs.count == rhs.count else { return false }
for i in lhs.indices {
if lhs[i].id != rhs[i].id || lhs[i].deliveryStatus != rhs[i].deliveryStatus {
return false
}
}
return true
}
.receive(on: DispatchQueue.main)
messagesPublisher
.sink { [weak self] newMessages in
self?.messages = newMessages
}
.store(in: &cancellables)
// Subscribe to typing state changes, filtered to our dialog
let typingPublisher = repo.$typingDialogs
.map { (dialogs: Set<String>) -> Bool in
dialogs.contains(key)
}
.removeDuplicates()
.receive(on: DispatchQueue.main)
typingPublisher
.sink { [weak self] typing in
self?.isTyping = typing
}
.store(in: &cancellables)
}
}