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