NativeMessageCell рефакторинг на UIView + wrapper для UICollectionView совместимости
This commit is contained in:
@@ -12,6 +12,7 @@ final class ChatMessageListItem: RosettaListItem {
|
||||
let message: ChatMessage
|
||||
let layout: MessageCellLayout
|
||||
let textLayout: CoreTextTextLayout?
|
||||
let timestampText: String
|
||||
let actions: MessageCellActions
|
||||
let replyName: String?
|
||||
let replyText: String?
|
||||
@@ -29,6 +30,7 @@ final class ChatMessageListItem: RosettaListItem {
|
||||
message: ChatMessage,
|
||||
layout: MessageCellLayout,
|
||||
textLayout: CoreTextTextLayout?,
|
||||
timestampText: String,
|
||||
actions: MessageCellActions,
|
||||
replyName: String? = nil,
|
||||
replyText: String? = nil,
|
||||
@@ -39,6 +41,7 @@ final class ChatMessageListItem: RosettaListItem {
|
||||
self.message = message
|
||||
self.layout = layout
|
||||
self.textLayout = textLayout
|
||||
self.timestampText = timestampText
|
||||
self.actions = actions
|
||||
self.replyName = replyName
|
||||
self.replyText = replyText
|
||||
@@ -86,7 +89,7 @@ final class ChatMessageListItem: RosettaListItem {
|
||||
node.cell.apply(layout: layout)
|
||||
node.cell.configure(
|
||||
message: message,
|
||||
timestamp: layout.timestampText,
|
||||
timestamp: timestampText,
|
||||
textLayout: textLayout,
|
||||
actions: actions,
|
||||
replyName: replyName,
|
||||
|
||||
@@ -10,7 +10,12 @@ import SwiftUI
|
||||
/// 3. No SwiftUI, no UIHostingConfiguration, no self-sizing
|
||||
///
|
||||
/// Subviews are always present but hidden when not needed (no alloc/dealloc overhead).
|
||||
final class NativeMessageCell: UICollectionViewCell {
|
||||
/// Base class: UIView (not UICollectionViewCell) — works in both RosettaListView and UICollectionView wrapper.
|
||||
final class NativeMessageCell: UIView {
|
||||
|
||||
/// Content container. UICollectionViewCell provides this automatically;
|
||||
/// as a plain UIView we create our own for API compatibility.
|
||||
let contentView = UIView()
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
@@ -299,6 +304,9 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
contentView.frame = bounds
|
||||
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
addSubview(contentView)
|
||||
setupViews()
|
||||
}
|
||||
|
||||
@@ -2144,13 +2152,9 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
|
||||
// MARK: - Self-sizing (from pre-calculated layout)
|
||||
|
||||
override func preferredLayoutAttributesFitting(
|
||||
_ layoutAttributes: UICollectionViewLayoutAttributes
|
||||
) -> UICollectionViewLayoutAttributes {
|
||||
// Always return concrete height — never fall to super (expensive self-sizing)
|
||||
let attrs = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
|
||||
attrs.size.height = currentLayout?.totalHeight ?? 50
|
||||
return attrs
|
||||
/// Effective height for external layout systems.
|
||||
var preferredHeight: CGFloat {
|
||||
currentLayout?.totalHeight ?? 50
|
||||
}
|
||||
|
||||
// MARK: - Link Tap
|
||||
@@ -3840,8 +3844,7 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
|
||||
// MARK: - Reuse
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
func prepareForReuse() {
|
||||
uploadOverlayShowTime = 0
|
||||
if let observer = uploadProgressObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
@@ -4516,3 +4519,43 @@ final class ForwardItemSubview: UIView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionView Wrapper
|
||||
|
||||
/// Thin UICollectionViewCell wrapper for NativeMessageCell (UIView).
|
||||
/// Allows existing UICollectionView-based NativeMessageList to continue working
|
||||
/// while NativeMessageCell itself is a plain UIView for RosettaListView.
|
||||
final class NativeMessageCollectionCell: UICollectionViewCell {
|
||||
|
||||
let messageView = NativeMessageCell(frame: .zero)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
messageView.frame = contentView.bounds
|
||||
messageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
contentView.addSubview(messageView)
|
||||
backgroundColor = .clear
|
||||
contentView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
messageView.prepareForReuse()
|
||||
}
|
||||
|
||||
override func preferredLayoutAttributesFitting(
|
||||
_ layoutAttributes: UICollectionViewLayoutAttributes
|
||||
) -> UICollectionViewLayoutAttributes {
|
||||
let attrs = layoutAttributes.copy() as! UICollectionViewLayoutAttributes
|
||||
attrs.size.height = messageView.preferredHeight
|
||||
return attrs
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
messageView.frame = contentView.bounds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,19 @@ final class NativeMessageListController: UIViewController {
|
||||
private var collectionView: UICollectionView!
|
||||
private static let unreadSeparatorId = "__unread_separator__"
|
||||
|
||||
/// Extract NativeMessageCell from a collection view cell (wrapper pattern).
|
||||
private func messageCell(from cell: UICollectionViewCell) -> NativeMessageCell? {
|
||||
(cell as? NativeMessageCollectionCell)?.messageView
|
||||
}
|
||||
|
||||
/// Extract NativeMessageCell at index path.
|
||||
private func messageCell(at indexPath: IndexPath) -> NativeMessageCell? {
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) else { return nil }
|
||||
return messageCell(from: cell)
|
||||
}
|
||||
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Int, String>!
|
||||
private var nativeCellRegistration: UICollectionView.CellRegistration<NativeMessageCell, ChatMessage>!
|
||||
private var nativeCellRegistration: UICollectionView.CellRegistration<NativeMessageCollectionCell, ChatMessage>!
|
||||
private var separatorCellRegistration: UICollectionView.CellRegistration<UnreadSeparatorCell, Bool>!
|
||||
|
||||
// MARK: - Insertion Animation
|
||||
@@ -348,7 +359,7 @@ final class NativeMessageListController: UIViewController {
|
||||
let sortedIndexPaths = collectionView.indexPathsForVisibleItems
|
||||
.sorted { $0.item < $1.item }
|
||||
for ip in sortedIndexPaths {
|
||||
guard let cell = collectionView.cellForItem(at: ip) as? NativeMessageCell,
|
||||
guard let cell = messageCell(at: ip),
|
||||
let msgId = messageId(for: ip) else { continue }
|
||||
let isOutgoing = layoutCache[msgId]?.isOutgoing ?? false
|
||||
// Hide cells before animation — they'll fade in via skeleton transition
|
||||
@@ -476,8 +487,9 @@ final class NativeMessageListController: UIViewController {
|
||||
}
|
||||
|
||||
private func setupNativeCellRegistration() {
|
||||
nativeCellRegistration = UICollectionView.CellRegistration<NativeMessageCell, ChatMessage> {
|
||||
[weak self] cell, indexPath, message in
|
||||
nativeCellRegistration = UICollectionView.CellRegistration<NativeMessageCollectionCell, ChatMessage> {
|
||||
[weak self] wrapper, indexPath, message in
|
||||
let cell = wrapper.messageView
|
||||
guard let self else { return }
|
||||
|
||||
// Patch group messages without attachment password (legacy messages before fix).
|
||||
@@ -1445,9 +1457,9 @@ final class NativeMessageListController: UIViewController {
|
||||
|
||||
// Update all visible cells
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let msgCell = cell as? NativeMessageCell else { continue }
|
||||
guard let msgCell = messageCell(from: cell) else { continue }
|
||||
msgCell.setSelectionMode(enabled, animated: animated)
|
||||
if enabled, let indexPath = collectionView.indexPath(for: msgCell),
|
||||
if enabled, let indexPath = collectionView.indexPath(for: cell),
|
||||
let msgId = messageId(for: indexPath) {
|
||||
msgCell.setMessageSelected(selectedMessageIds.contains(msgId), animated: false)
|
||||
}
|
||||
@@ -1462,8 +1474,9 @@ final class NativeMessageListController: UIViewController {
|
||||
selectedMessageIds = ids
|
||||
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let msgCell = cell as? NativeMessageCell,
|
||||
let indexPath = collectionView.indexPath(for: msgCell),
|
||||
guard let wrapper = cell as? NativeMessageCollectionCell,
|
||||
let msgCell = messageCell(from: wrapper),
|
||||
let indexPath = collectionView.indexPath(for: wrapper),
|
||||
let msgId = messageId(for: indexPath) else { continue }
|
||||
let wasSelected = oldIds.contains(msgId)
|
||||
let isSelected = ids.contains(msgId)
|
||||
@@ -1889,13 +1902,13 @@ final class NativeMessageListController: UIViewController {
|
||||
func animateHighlight(messageId: String?) {
|
||||
// Hide any previously highlighted cell
|
||||
for cell in collectionView.visibleCells {
|
||||
(cell as? NativeMessageCell)?.hideHighlight()
|
||||
messageCell(from: cell)?.hideHighlight()
|
||||
}
|
||||
guard let messageId,
|
||||
let snapshot = dataSource?.snapshot(),
|
||||
let itemIndex = snapshot.indexOfItem(messageId) else { return }
|
||||
let indexPath = IndexPath(item: itemIndex, section: 0)
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? NativeMessageCell else { return }
|
||||
guard let cell = messageCell(at: indexPath) else { return }
|
||||
cell.showHighlight()
|
||||
}
|
||||
|
||||
@@ -2214,7 +2227,7 @@ extension NativeMessageListController: ComposerViewDelegate {
|
||||
let player = VoiceMessagePlayer.shared
|
||||
guard let playingId = player.currentMessageId else { return }
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let messageCell = cell as? NativeMessageCell else { continue }
|
||||
guard let messageCell = self.messageCell(from: cell) else { continue }
|
||||
// Match main voice view OR any forward item voice view
|
||||
let matchesMain = messageCell.currentMessageId == playingId
|
||||
let matchesForward = messageCell.hasForwardVoiceWithPlaybackId(playingId)
|
||||
@@ -2232,7 +2245,7 @@ extension NativeMessageListController: ComposerViewDelegate {
|
||||
private func updateAllVisibleVoiceCells() {
|
||||
let player = VoiceMessagePlayer.shared
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let messageCell = cell as? NativeMessageCell else { continue }
|
||||
guard let messageCell = self.messageCell(from: cell) else { continue }
|
||||
let isThisMessage = messageCell.currentMessageId == player.currentMessageId
|
||||
|| messageCell.hasForwardVoiceWithPlaybackId(player.currentMessageId)
|
||||
messageCell.updateVoicePlayback(
|
||||
@@ -2409,7 +2422,7 @@ extension NativeMessageListController: ComposerViewDelegate {
|
||||
collectionView.scrollToItem(at: indexPath, at: .bottom, animated: false)
|
||||
collectionView.layoutIfNeeded()
|
||||
}
|
||||
guard let cell = collectionView.cellForItem(at: indexPath) as? NativeMessageCell else {
|
||||
guard let cell = messageCell(at: indexPath) else {
|
||||
return nil
|
||||
}
|
||||
return cell.voiceTransitionTargetFrame(in: window) ?? cell.bubbleFrameInWindow(window)
|
||||
|
||||
Reference in New Issue
Block a user