RosettaMessageListController минимальный контроллер для кастомного ListView
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import UIKit
|
||||
import os
|
||||
|
||||
// MARK: - RosettaMessageListController
|
||||
|
||||
/// Chat message list using custom RosettaListView (Telegram-parity).
|
||||
/// Replaces NativeMessageListController (UICollectionView-based).
|
||||
///
|
||||
/// Phase 1: Basic message display + scroll. No keyboard, date pills, selection.
|
||||
@MainActor
|
||||
final class RosettaMessageListController: UIViewController {
|
||||
|
||||
private static let perfLog = Logger(subsystem: "com.rosetta.messenger", category: "RosettaList")
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
struct Config {
|
||||
var maxBubbleWidth: CGFloat
|
||||
var currentPublicKey: String
|
||||
var opponentPublicKey: String
|
||||
var opponentTitle: String
|
||||
var isGroupChat: Bool
|
||||
var groupAdminKey: String
|
||||
var actions: MessageCellActions
|
||||
}
|
||||
|
||||
private let config: Config
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
private let listView = RosettaListView(frame: .zero)
|
||||
|
||||
// MARK: - State
|
||||
|
||||
private var messages: [ChatMessage] = []
|
||||
private var layoutCache: [String: MessageCellLayout] = [:]
|
||||
private var textLayoutCache: [String: CoreTextTextLayout] = [:]
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init(config: Config) {
|
||||
self.config = config
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .clear
|
||||
|
||||
listView.frame = view.bounds
|
||||
listView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
listView.stackFromBottom = true
|
||||
view.addSubview(listView)
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Update displayed messages. Calculates layouts and builds list items.
|
||||
func update(messages: [ChatMessage]) {
|
||||
let start = CACurrentMediaTime()
|
||||
self.messages = messages
|
||||
|
||||
// Calculate layouts for new messages (reuse cached)
|
||||
calculateLayouts()
|
||||
|
||||
// Build list items
|
||||
var items: [RosettaListItem] = []
|
||||
for message in messages {
|
||||
guard let layout = layoutCache[message.id] else { continue }
|
||||
let textLayout = textLayoutCache[message.id]
|
||||
let item = ChatMessageListItem(
|
||||
message: message,
|
||||
layout: layout,
|
||||
textLayout: textLayout,
|
||||
timestampText: formatTimestamp(message.timestamp),
|
||||
actions: config.actions
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
listView.setItems(items)
|
||||
|
||||
let ms = (CACurrentMediaTime() - start) * 1000
|
||||
Self.perfLog.notice("⚡ RosettaList.update: \(messages.count, privacy: .public) msgs in \(String(format: "%.0f", ms), privacy: .public)ms")
|
||||
}
|
||||
|
||||
/// Scroll to newest message.
|
||||
func scrollToBottom(animated: Bool) {
|
||||
listView.scrollToBottom(animated: animated)
|
||||
}
|
||||
|
||||
// MARK: - Layout Calculation
|
||||
|
||||
private func calculateLayouts() {
|
||||
let isDark = traitCollection.userInterfaceStyle == .dark
|
||||
let (layouts, textLayouts) = MessageCellLayout.batchCalculate(
|
||||
messages: messages,
|
||||
maxBubbleWidth: config.maxBubbleWidth,
|
||||
currentPublicKey: config.currentPublicKey,
|
||||
opponentPublicKey: config.opponentPublicKey,
|
||||
opponentTitle: config.opponentTitle,
|
||||
isGroupChat: config.isGroupChat,
|
||||
groupAdminKey: config.groupAdminKey,
|
||||
isDarkMode: isDark,
|
||||
dirtyIds: nil,
|
||||
existingLayouts: layoutCache.isEmpty ? nil : layoutCache,
|
||||
existingTextLayouts: textLayoutCache.isEmpty ? nil : textLayoutCache
|
||||
)
|
||||
layoutCache = layouts
|
||||
textLayoutCache = textLayouts
|
||||
}
|
||||
|
||||
// MARK: - Timestamp Formatting
|
||||
|
||||
private static let timestampFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateFormat = "HH:mm"
|
||||
f.locale = .autoupdatingCurrent
|
||||
f.timeZone = .autoupdatingCurrent
|
||||
return f
|
||||
}()
|
||||
|
||||
private func formatTimestamp(_ ms: Int64) -> String {
|
||||
Self.timestampFormatter.string(from: Date(timeIntervalSince1970: Double(ms) / 1000))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user