Фича: reply-to-reply + подсветка сообщения при навигации по реплаю
This commit is contained in:
@@ -138,6 +138,9 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
private let forwardAvatarView = UIView()
|
||||
private let forwardNameLabel = UILabel()
|
||||
|
||||
// Highlight overlay (scroll-to-message flash)
|
||||
private let highlightOverlay = UIView()
|
||||
|
||||
// Swipe-to-reply
|
||||
private let replyCircleView = UIView()
|
||||
private let replyIconView = UIImageView()
|
||||
@@ -404,6 +407,13 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
forwardNameLabel.textColor = .white
|
||||
bubbleView.addSubview(forwardNameLabel)
|
||||
|
||||
// Highlight overlay — on top of all bubble content
|
||||
highlightOverlay.backgroundColor = UIColor.white.withAlphaComponent(0.12)
|
||||
highlightOverlay.isUserInteractionEnabled = false
|
||||
highlightOverlay.alpha = 0
|
||||
highlightOverlay.layer.cornerCurve = .continuous
|
||||
bubbleView.addSubview(highlightOverlay)
|
||||
|
||||
// Swipe reply icon — circle + Telegram-exact arrow (same vector as SwiftUI SwipeToReplyModifier)
|
||||
replyCircleView.backgroundColor = UIColor.white.withAlphaComponent(0.12)
|
||||
replyCircleView.layer.cornerRadius = 17 // 34pt / 2
|
||||
@@ -749,6 +759,10 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
outgoing: layout.isOutgoing, mergeType: layout.mergeType
|
||||
)
|
||||
|
||||
// Highlight overlay — matches bubble bounds with inner corner radius
|
||||
highlightOverlay.frame = bubbleView.bounds
|
||||
highlightOverlay.layer.cornerRadius = 16
|
||||
|
||||
// ── Vector shadow path (approximate shape, used only for shadow) ──
|
||||
bubbleLayer.frame = bubbleView.bounds
|
||||
let shapeRect = imageFrame
|
||||
@@ -1074,9 +1088,8 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
// MARK: - Swipe to Reply
|
||||
|
||||
@objc private func handleSwipe(_ gesture: UIPanGestureRecognizer) {
|
||||
// Desktop parity: system accounts + ATTACHMENTS_NOT_ALLOWED_TO_REPLY = [AVATAR, MESSAGES]
|
||||
if isSavedMessages || isSystemAccount { return }
|
||||
let isReplyBlocked = message?.attachments.contains(where: { $0.type == .avatar || $0.type == .messages }) ?? false
|
||||
let isReplyBlocked = message?.attachments.contains(where: { $0.type == .avatar }) ?? false
|
||||
if isReplyBlocked { return }
|
||||
|
||||
let translation = gesture.translation(in: contentView)
|
||||
@@ -1304,6 +1317,21 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Highlight (scroll-to-message flash)
|
||||
|
||||
func showHighlight() {
|
||||
highlightOverlay.alpha = 0
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.highlightOverlay.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
func hideHighlight() {
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.highlightOverlay.alpha = 0
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func replyQuoteTapped() {
|
||||
guard let replyMessageId, let actions else { return }
|
||||
actions.onScrollToMessage(replyMessageId)
|
||||
@@ -2052,6 +2080,7 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
resetPhotoTiles()
|
||||
replyContainer.isHidden = true
|
||||
replyMessageId = nil
|
||||
highlightOverlay.alpha = 0
|
||||
fileContainer.isHidden = true
|
||||
callArrowView.isHidden = true
|
||||
callBackButton.isHidden = true
|
||||
|
||||
@@ -1028,6 +1028,20 @@ final class NativeMessageListController: UIViewController {
|
||||
collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: animated)
|
||||
}
|
||||
|
||||
/// Telegram-style highlight flash on a message cell.
|
||||
func animateHighlight(messageId: String?) {
|
||||
// Hide any previously highlighted cell
|
||||
for cell in collectionView.visibleCells {
|
||||
(cell as? NativeMessageCell)?.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 }
|
||||
cell.showHighlight()
|
||||
}
|
||||
|
||||
/// Reconfigure visible cells without rebuilding the snapshot.
|
||||
func reconfigureVisibleCells() {
|
||||
var snapshot = dataSource.snapshot()
|
||||
@@ -1373,9 +1387,9 @@ struct NativeMessageListView: UIViewControllerRepresentable {
|
||||
controller.config.maxBubbleWidth = maxBubbleWidth
|
||||
configChanged = true
|
||||
}
|
||||
if controller.config.highlightedMessageId != highlightedMessageId {
|
||||
let highlightChanged = controller.config.highlightedMessageId != highlightedMessageId
|
||||
if highlightChanged {
|
||||
controller.config.highlightedMessageId = highlightedMessageId
|
||||
configChanged = true
|
||||
}
|
||||
if controller.config.firstUnreadMessageId != firstUnreadMessageId {
|
||||
controller.config.firstUnreadMessageId = firstUnreadMessageId
|
||||
@@ -1435,6 +1449,11 @@ struct NativeMessageListView: UIViewControllerRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight animation (Telegram-style flash on scroll-to-message)
|
||||
if highlightChanged {
|
||||
controller.animateHighlight(messageId: highlightedMessageId)
|
||||
}
|
||||
|
||||
// Empty state (iOS < 26 — UIKit-managed for keyboard animation parity)
|
||||
if let info = emptyChatInfo {
|
||||
controller.updateEmptyState(isEmpty: messages.isEmpty, info: info)
|
||||
|
||||
Reference in New Issue
Block a user