NativeMessageCell рефакторинг на UIView + wrapper для UICollectionView совместимости

This commit is contained in:
2026-04-18 12:29:27 +05:00
parent dedef48a55
commit 2f80ab5cc1
3 changed files with 83 additions and 24 deletions

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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)