Фича: reply-to-reply + подсветка сообщения при навигации по реплаю

This commit is contained in:
2026-03-31 03:07:38 +05:00
parent 6b55baacd8
commit 876e541006
2 changed files with 52 additions and 4 deletions

View File

@@ -138,6 +138,9 @@ final class NativeMessageCell: UICollectionViewCell {
private let forwardAvatarView = UIView() private let forwardAvatarView = UIView()
private let forwardNameLabel = UILabel() private let forwardNameLabel = UILabel()
// Highlight overlay (scroll-to-message flash)
private let highlightOverlay = UIView()
// Swipe-to-reply // Swipe-to-reply
private let replyCircleView = UIView() private let replyCircleView = UIView()
private let replyIconView = UIImageView() private let replyIconView = UIImageView()
@@ -404,6 +407,13 @@ final class NativeMessageCell: UICollectionViewCell {
forwardNameLabel.textColor = .white forwardNameLabel.textColor = .white
bubbleView.addSubview(forwardNameLabel) 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) // Swipe reply icon circle + Telegram-exact arrow (same vector as SwiftUI SwipeToReplyModifier)
replyCircleView.backgroundColor = UIColor.white.withAlphaComponent(0.12) replyCircleView.backgroundColor = UIColor.white.withAlphaComponent(0.12)
replyCircleView.layer.cornerRadius = 17 // 34pt / 2 replyCircleView.layer.cornerRadius = 17 // 34pt / 2
@@ -749,6 +759,10 @@ final class NativeMessageCell: UICollectionViewCell {
outgoing: layout.isOutgoing, mergeType: layout.mergeType 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) // Vector shadow path (approximate shape, used only for shadow)
bubbleLayer.frame = bubbleView.bounds bubbleLayer.frame = bubbleView.bounds
let shapeRect = imageFrame let shapeRect = imageFrame
@@ -1074,9 +1088,8 @@ final class NativeMessageCell: UICollectionViewCell {
// MARK: - Swipe to Reply // MARK: - Swipe to Reply
@objc private func handleSwipe(_ gesture: UIPanGestureRecognizer) { @objc private func handleSwipe(_ gesture: UIPanGestureRecognizer) {
// Desktop parity: system accounts + ATTACHMENTS_NOT_ALLOWED_TO_REPLY = [AVATAR, MESSAGES]
if isSavedMessages || isSystemAccount { return } 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 } if isReplyBlocked { return }
let translation = gesture.translation(in: contentView) 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() { @objc private func replyQuoteTapped() {
guard let replyMessageId, let actions else { return } guard let replyMessageId, let actions else { return }
actions.onScrollToMessage(replyMessageId) actions.onScrollToMessage(replyMessageId)
@@ -2052,6 +2080,7 @@ final class NativeMessageCell: UICollectionViewCell {
resetPhotoTiles() resetPhotoTiles()
replyContainer.isHidden = true replyContainer.isHidden = true
replyMessageId = nil replyMessageId = nil
highlightOverlay.alpha = 0
fileContainer.isHidden = true fileContainer.isHidden = true
callArrowView.isHidden = true callArrowView.isHidden = true
callBackButton.isHidden = true callBackButton.isHidden = true

View File

@@ -1028,6 +1028,20 @@ final class NativeMessageListController: UIViewController {
collectionView.scrollToItem(at: indexPath, at: .centeredVertically, animated: animated) 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. /// Reconfigure visible cells without rebuilding the snapshot.
func reconfigureVisibleCells() { func reconfigureVisibleCells() {
var snapshot = dataSource.snapshot() var snapshot = dataSource.snapshot()
@@ -1373,9 +1387,9 @@ struct NativeMessageListView: UIViewControllerRepresentable {
controller.config.maxBubbleWidth = maxBubbleWidth controller.config.maxBubbleWidth = maxBubbleWidth
configChanged = true configChanged = true
} }
if controller.config.highlightedMessageId != highlightedMessageId { let highlightChanged = controller.config.highlightedMessageId != highlightedMessageId
if highlightChanged {
controller.config.highlightedMessageId = highlightedMessageId controller.config.highlightedMessageId = highlightedMessageId
configChanged = true
} }
if controller.config.firstUnreadMessageId != firstUnreadMessageId { if controller.config.firstUnreadMessageId != firstUnreadMessageId {
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) // Empty state (iOS < 26 UIKit-managed for keyboard animation parity)
if let info = emptyChatInfo { if let info = emptyChatInfo {
controller.updateEmptyState(isEmpty: messages.isEmpty, info: info) controller.updateEmptyState(isEmpty: messages.isEmpty, info: info)