Фича: reply-to-reply + подсветка сообщения при навигации по реплаю
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user