Форвард: Telegram-parity UI — правильный размер бабла, текст/таймстамп, аватарка с инициалами, отступы

This commit is contained in:
2026-03-31 16:36:58 +05:00
parent e5179b11ea
commit 464fae37a9
11 changed files with 1037 additions and 53 deletions

View File

@@ -242,6 +242,7 @@ struct ChatDetailView: View {
// setDialogActive only touches MessageRepository.activeDialogs (Set),
// does NOT mutate DialogRepository, so ForEach won't rebuild.
MessageRepository.shared.setDialogActive(route.publicKey, isActive: true)
SessionManager.shared.resetIdleTimer()
updateReadEligibility()
clearDeliveredNotifications(for: route.publicKey)
// Telegram-like read policy: mark read only when dialog is truly readable
@@ -275,6 +276,7 @@ struct ChatDetailView: View {
isViewActive = false
updateReadEligibility()
MessageRepository.shared.setDialogActive(route.publicKey, isActive: false)
SessionManager.shared.stopIdleTimer()
// Desktop parity: save draft text on chat close.
DraftManager.shared.saveDraft(for: route.publicKey, text: messageText)
}
@@ -285,6 +287,7 @@ struct ChatDetailView: View {
// 600ms delay lets notification-tap navigation settle if user tapped
// a notification for a DIFFERENT chat, isViewActive becomes false.
guard isViewActive else { return }
SessionManager.shared.resetIdleTimer()
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(600))
guard isViewActive else { return }
@@ -838,6 +841,7 @@ private extension ChatDetailView {
scrollToBottomRequested: $scrollToBottomRequested,
onAtBottomChange: { atBottom in
isAtBottom = atBottom
SessionManager.shared.resetIdleTimer()
updateReadEligibility()
if atBottom {
markDialogAsRead()

View File

@@ -172,25 +172,25 @@ struct MessageCellView: View, Equatable {
VStack(alignment: .leading, spacing: 0) {
Text("Forwarded from")
.font(.system(size: 13, weight: .regular))
.foregroundStyle(outgoing ? Color.white.opacity(0.5) : RosettaColors.Adaptive.textSecondary)
.font(.system(size: 14, weight: .regular))
.foregroundStyle(outgoing ? Color.white : RosettaColors.figmaBlue)
.padding(.leading, 11)
.padding(.top, 6)
.padding(.top, 7)
HStack(spacing: 6) {
HStack(spacing: 4) {
AvatarView(
initials: senderInitials,
colorIndex: senderColorIndex,
size: 20,
size: 16,
image: senderAvatar
)
Text(senderName)
.font(.system(size: 14, weight: .semibold))
.font(.system(size: 14, weight: .medium))
.foregroundStyle(outgoing ? Color.white : RosettaColors.figmaBlue)
.lineLimit(1)
}
.padding(.leading, 11)
.padding(.top, 3)
.padding(.top, 0)
if !imageAttachments.isEmpty {
ForwardedPhotoCollageView(

View File

@@ -19,8 +19,8 @@ final class NativeMessageCell: UICollectionViewCell {
private static let timestampFont = UIFont.systemFont(ofSize: floor(textFont.pointSize * 11.0 / 17.0), weight: .regular)
private static let replyNameFont = UIFont.systemFont(ofSize: 14, weight: .semibold)
private static let replyTextFont = UIFont.systemFont(ofSize: 14, weight: .regular)
private static let forwardLabelFont = UIFont.systemFont(ofSize: 13, weight: .regular)
private static let forwardNameFont = UIFont.systemFont(ofSize: 14, weight: .semibold)
private static let forwardLabelFont = UIFont.systemFont(ofSize: 14, weight: .regular)
private static let forwardNameFont = UIFont.systemFont(ofSize: 14, weight: .medium)
private static let fileNameFont = UIFont.systemFont(ofSize: 16, weight: .regular)
private static let fileSizeFont = UIFont.monospacedDigitSystemFont(ofSize: 13, weight: .regular)
private static let bubbleMetrics = BubbleMetrics.telegram()
@@ -136,6 +136,8 @@ final class NativeMessageCell: UICollectionViewCell {
// Forward header
private let forwardLabel = UILabel()
private let forwardAvatarView = UIView()
private let forwardAvatarInitialLabel = UILabel()
private let forwardAvatarImageView = UIImageView()
private let forwardNameLabel = UILabel()
// Highlight overlay (scroll-to-message flash)
@@ -397,16 +399,24 @@ final class NativeMessageCell: UICollectionViewCell {
// Forward header
forwardLabel.font = Self.forwardLabelFont
forwardLabel.text = "Forwarded message"
forwardLabel.textColor = UIColor.white.withAlphaComponent(0.6)
forwardLabel.text = "Forwarded from"
bubbleView.addSubview(forwardLabel)
forwardAvatarView.backgroundColor = UIColor.white.withAlphaComponent(0.3)
forwardAvatarView.layer.cornerRadius = 10
forwardAvatarView.layer.cornerRadius = 8
forwardAvatarView.clipsToBounds = true
bubbleView.addSubview(forwardAvatarView)
forwardAvatarInitialLabel.font = .systemFont(ofSize: 8, weight: .medium)
forwardAvatarInitialLabel.textColor = .white
forwardAvatarInitialLabel.textAlignment = .center
forwardAvatarView.addSubview(forwardAvatarInitialLabel)
forwardAvatarImageView.contentMode = .scaleAspectFill
forwardAvatarImageView.clipsToBounds = true
forwardAvatarView.addSubview(forwardAvatarImageView)
forwardNameLabel.font = Self.forwardNameFont
forwardNameLabel.textColor = .white
forwardNameLabel.textColor = .white // default, overridden in configure()
bubbleView.addSubview(forwardNameLabel)
// Highlight overlay on top of all bubble content
@@ -457,7 +467,8 @@ final class NativeMessageCell: UICollectionViewCell {
replyName: String? = nil,
replyText: String? = nil,
replyMessageId: String? = nil,
forwardSenderName: String? = nil
forwardSenderName: String? = nil,
forwardSenderKey: String? = nil
) {
self.message = message
self.actions = actions
@@ -542,6 +553,31 @@ final class NativeMessageCell: UICollectionViewCell {
forwardAvatarView.isHidden = false
forwardNameLabel.isHidden = false
forwardNameLabel.text = forwardSenderName
// Telegram: same accentTextColor for both title and name
let accent: UIColor = isOutgoing ? .white : Self.outgoingColor
forwardLabel.textColor = accent
forwardNameLabel.textColor = accent
// Avatar: real photo if available, otherwise initial + color
if let key = forwardSenderKey, let avatarImage = AvatarRepository.shared.loadAvatar(publicKey: key) {
forwardAvatarImageView.image = avatarImage
forwardAvatarImageView.isHidden = false
forwardAvatarInitialLabel.isHidden = true
forwardAvatarView.backgroundColor = .clear
} else {
forwardAvatarImageView.image = nil
forwardAvatarImageView.isHidden = true
forwardAvatarInitialLabel.isHidden = false
let initial = String(forwardSenderName.prefix(1)).uppercased()
forwardAvatarInitialLabel.text = initial
let colorIndex = RosettaColors.avatarColorIndex(for: forwardSenderName, publicKey: forwardSenderKey ?? "")
let hexes: [UInt32] = [0x228be6, 0x15aabf, 0xbe4bdb, 0x40c057, 0x4c6ef5, 0x82c91e, 0xfd7e14, 0xe64980, 0xfa5252, 0x12b886, 0x7950f2]
let hex = hexes[colorIndex % hexes.count]
forwardAvatarView.backgroundColor = UIColor(
red: CGFloat((hex >> 16) & 0xFF) / 255,
green: CGFloat((hex >> 8) & 0xFF) / 255,
blue: CGFloat(hex & 0xFF) / 255, alpha: 1
)
}
} else {
forwardLabel.isHidden = true
forwardAvatarView.isHidden = true
@@ -894,6 +930,9 @@ final class NativeMessageCell: UICollectionViewCell {
if layout.isForward {
forwardLabel.frame = layout.forwardHeaderFrame
forwardAvatarView.frame = layout.forwardAvatarFrame
let avatarBounds = forwardAvatarView.bounds
forwardAvatarInitialLabel.frame = avatarBounds
forwardAvatarImageView.frame = avatarBounds
forwardNameLabel.frame = layout.forwardNameFrame
}

View File

@@ -271,6 +271,7 @@ final class NativeMessageListController: UIViewController {
var replyText: String?
var replyMessageId: String?
var forwardSenderName: String?
var forwardSenderKey: String?
if let att = replyAtt {
if let data = att.blob.data(using: .utf8),
@@ -293,6 +294,7 @@ final class NativeMessageListController: UIViewController {
if displayText.isEmpty {
// Forward
forwardSenderName = name
forwardSenderKey = senderKey
} else {
// Reply quote
replyName = name
@@ -312,7 +314,8 @@ final class NativeMessageListController: UIViewController {
replyName: replyName,
replyText: replyText,
replyMessageId: replyMessageId,
forwardSenderName: forwardSenderName
forwardSenderName: forwardSenderName,
forwardSenderKey: forwardSenderKey
)
}
}