Реплай: исправлен отступ от бара до текста (6pt → 8pt, Telegram parity)
This commit is contained in:
@@ -278,6 +278,20 @@ struct ChatDetailView: View {
|
||||
// Desktop parity: save draft text on chat close.
|
||||
DraftManager.shared.saveDraft(for: route.publicKey, text: messageText)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||||
// Re-evaluate read eligibility after app returns from background.
|
||||
// readEligibleDialogs is cleared on didEnterBackground (SessionManager);
|
||||
// this restores eligibility for the currently-visible chat.
|
||||
// 600ms delay lets notification-tap navigation settle — if user tapped
|
||||
// a notification for a DIFFERENT chat, isViewActive becomes false.
|
||||
guard isViewActive else { return }
|
||||
Task { @MainActor in
|
||||
try? await Task.sleep(for: .milliseconds(600))
|
||||
guard isViewActive else { return }
|
||||
updateReadEligibility()
|
||||
markDialogAsRead()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -931,9 +945,6 @@ private extension ChatDetailView {
|
||||
},
|
||||
onUserTextInsertion: handleComposerUserTyping,
|
||||
onMultilineChange: { multiline in
|
||||
#if DEBUG
|
||||
print("📐 onMultilineChange callback: \(multiline) (was \(isMultilineInput)) → radius will be \(multiline ? 16 : 21)")
|
||||
#endif
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
isMultilineInput = multiline
|
||||
}
|
||||
@@ -1276,18 +1287,6 @@ private extension ChatDetailView {
|
||||
// MARK: - Forward
|
||||
|
||||
func forwardMessage(_ message: ChatMessage, to targetRoute: ChatRoute) {
|
||||
#if DEBUG
|
||||
print("═══════════════════════════════════════════════")
|
||||
print("📤 FORWARD START")
|
||||
print("📤 Original message: id=\(message.id.prefix(16)), text='\(message.text.prefix(30))'")
|
||||
print("📤 Original attachments (\(message.attachments.count)):")
|
||||
for att in message.attachments {
|
||||
print("📤 - type=\(att.type) id=\(att.id.prefix(16)) preview='\(att.preview.prefix(40))' blob=\(att.blob.isEmpty ? "(empty)" : "(\(att.blob.count) chars, starts: \(att.blob.prefix(30)))")")
|
||||
}
|
||||
print("📤 Attachment password: \(message.attachmentPassword?.prefix(20) ?? "nil")")
|
||||
print("📤 Target: \(targetRoute.publicKey.prefix(16))")
|
||||
#endif
|
||||
|
||||
// Android parity: unwrap nested forwards.
|
||||
// If the message being forwarded is itself a forward, extract the inner
|
||||
// forwarded messages and re-forward them directly (flatten).
|
||||
@@ -1296,43 +1295,15 @@ private extension ChatDetailView {
|
||||
let replyAttachment = message.attachments.first(where: { $0.type == .messages })
|
||||
let isForward = message.text.trimmingCharacters(in: .whitespaces).isEmpty
|
||||
|
||||
#if DEBUG
|
||||
if let att = replyAttachment {
|
||||
let blobParsed = parseReplyBlob(att.blob)
|
||||
let previewParsed = parseReplyBlob(att.preview)
|
||||
print("📤 Unwrap check: isForward=\(isForward)")
|
||||
print("📤 blob parse: \(blobParsed == nil ? "FAILED" : "OK (\(blobParsed!.count) msgs, atts: \(blobParsed!.map { $0.attachments.count }))")")
|
||||
print("📤 preview parse: \(previewParsed == nil ? "FAILED (preview='\(att.preview.prefix(20))')" : "OK (\(previewParsed!.count) msgs)")")
|
||||
}
|
||||
#endif
|
||||
|
||||
if isForward,
|
||||
let att = replyAttachment,
|
||||
let innerMessages = (parseReplyBlob(att.blob) ?? parseReplyBlob(att.preview)),
|
||||
!innerMessages.isEmpty {
|
||||
// Unwrap: forward the original messages, not the wrapper
|
||||
forwardDataList = innerMessages
|
||||
#if DEBUG
|
||||
print("📤 ✅ UNWRAP path: \(innerMessages.count) inner message(s)")
|
||||
for (i, msg) in innerMessages.enumerated() {
|
||||
print("📤 msg[\(i)]: publicKey=\(msg.publicKey.prefix(12)), text='\(msg.message.prefix(30))', attachments=\(msg.attachments.count)")
|
||||
for att in msg.attachments {
|
||||
print("📤 att: type=\(att.type) id=\(att.id.prefix(16)) preview='\(att.preview.prefix(40))'")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// Regular message — forward as-is
|
||||
forwardDataList = [buildReplyData(from: message)]
|
||||
#if DEBUG
|
||||
print("📤 ⚠️ BUILD_REPLY_DATA path (unwrap failed or not a forward)")
|
||||
if let first = forwardDataList.first {
|
||||
print("📤 result: publicKey=\(first.publicKey.prefix(12)), text='\(first.message.prefix(30))', attachments=\(first.attachments.count)")
|
||||
for att in first.attachments {
|
||||
print("📤 att: type=\(att.type) id=\(att.id.prefix(16)) preview='\(att.preview.prefix(40))'")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Desktop commit aaa4b42: no re-upload needed.
|
||||
@@ -1343,14 +1314,6 @@ private extension ChatDetailView {
|
||||
let targetUsername = targetRoute.username
|
||||
|
||||
Task { @MainActor in
|
||||
#if DEBUG
|
||||
print("📤 ── SEND SUMMARY ──")
|
||||
print("📤 forwardDataList: \(forwardDataList.count) message(s)")
|
||||
for (i, msg) in forwardDataList.enumerated() {
|
||||
print("📤 msg[\(i)]: text='\(msg.message.prefix(20))' attachments=\(msg.attachments.count) chacha_key_plain=\(msg.chacha_key_plain.prefix(16))…")
|
||||
}
|
||||
#endif
|
||||
|
||||
do {
|
||||
try await SessionManager.shared.sendMessageWithReply(
|
||||
text: "",
|
||||
@@ -1359,15 +1322,7 @@ private extension ChatDetailView {
|
||||
opponentTitle: targetTitle,
|
||||
opponentUsername: targetUsername
|
||||
)
|
||||
#if DEBUG
|
||||
print("📤 ✅ FORWARD SENT OK")
|
||||
print("═══════════════════════════════════════════════")
|
||||
#endif
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("📤 ❌ FORWARD FAILED: \(error)")
|
||||
print("═══════════════════════════════════════════════")
|
||||
#endif
|
||||
sendError = "Failed to forward message"
|
||||
}
|
||||
}
|
||||
@@ -1400,9 +1355,6 @@ private extension ChatDetailView {
|
||||
let msgAtt = message.attachments.first(where: { $0.type == .messages }),
|
||||
let innerMessages = parseReplyBlob(msgAtt.blob),
|
||||
let firstInner = innerMessages.first {
|
||||
#if DEBUG
|
||||
print("📤 buildReplyData: extracted inner message with \(firstInner.attachments.count) attachments")
|
||||
#endif
|
||||
return firstInner
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ struct ForwardChatPickerView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let _ = print("[ForwardPicker] body — dialogs count: \(dialogs.count)")
|
||||
NavigationStack {
|
||||
List(dialogs) { dialog in
|
||||
Button {
|
||||
|
||||
@@ -235,17 +235,11 @@ struct MessageAvatarView: View {
|
||||
|
||||
let tag = attachment.effectiveDownloadTag
|
||||
guard !tag.isEmpty else {
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR FAIL: empty tag for attachment \(attachment.id), preview=\(attachment.preview.prefix(60)), transportTag=\(attachment.transportTag)")
|
||||
#endif
|
||||
downloadError = true
|
||||
return
|
||||
}
|
||||
|
||||
guard let storedPassword = message.attachmentPassword, !storedPassword.isEmpty else {
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR FAIL: nil/empty attachmentPassword for msgId=\(message.id.prefix(8))… attId=\(attachment.id)")
|
||||
#endif
|
||||
downloadError = true
|
||||
return
|
||||
}
|
||||
@@ -254,23 +248,12 @@ struct MessageAvatarView: View {
|
||||
downloadError = false
|
||||
|
||||
let server = attachment.transportServer
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR START: tag=\(tag) server=\(server) storedPwd=\(storedPassword.prefix(30))… attId=\(attachment.id)")
|
||||
#endif
|
||||
Task {
|
||||
do {
|
||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||
|
||||
#if DEBUG
|
||||
let hasColon = encryptedString.contains(":")
|
||||
print("🖼️ AVATAR DOWNLOADED: \(encryptedData.count) bytes, hasColon=\(hasColon), first50=\(encryptedString.prefix(50))")
|
||||
#endif
|
||||
|
||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR CANDIDATES (\(passwords.count)): \(passwords.map { "\($0.prefix(20))…(\($0.count)ch)" })")
|
||||
#endif
|
||||
let downloadedImage = decryptAndParseImage(
|
||||
encryptedString: encryptedString, passwords: passwords
|
||||
)
|
||||
@@ -285,21 +268,12 @@ struct MessageAvatarView: View {
|
||||
let base64 = jpegData.base64EncodedString()
|
||||
AvatarRepository.shared.saveAvatarFromBase64(base64, publicKey: senderKey)
|
||||
}
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR DECRYPT OK: attId=\(attachment.id)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR DECRYPT FAIL: all \(passwords.count) candidates failed for attId=\(attachment.id)")
|
||||
#endif
|
||||
downloadError = true
|
||||
}
|
||||
isDownloading = false
|
||||
}
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("🖼️ AVATAR ERROR: \(error) for tag=\(tag)")
|
||||
#endif
|
||||
await MainActor.run {
|
||||
downloadError = true
|
||||
isDownloading = false
|
||||
@@ -311,41 +285,25 @@ struct MessageAvatarView: View {
|
||||
/// Tries each password candidate and validates the decrypted content is a real image.
|
||||
private func decryptAndParseImage(encryptedString: String, passwords: [String]) -> UIImage? {
|
||||
let crypto = CryptoManager.shared
|
||||
for (i, password) in passwords.enumerated() {
|
||||
for password in passwords {
|
||||
do {
|
||||
let data = try crypto.decryptWithPassword(
|
||||
encryptedString, password: password, requireCompression: true
|
||||
)
|
||||
#if DEBUG
|
||||
print("🖼️ PASS1 candidate[\(i)] decrypted \(data.count) bytes")
|
||||
#endif
|
||||
if let img = parseImageData(data) { return img }
|
||||
#if DEBUG
|
||||
print("🖼️ PASS1 candidate[\(i)] parseImageData FAILED")
|
||||
#endif
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("🖼️ PASS1 candidate[\(i)] FAILED: \(error)")
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Fallback: try without requireCompression (legacy uncompressed payloads)
|
||||
for (i, password) in passwords.enumerated() {
|
||||
for password in passwords {
|
||||
do {
|
||||
let data = try crypto.decryptWithPassword(
|
||||
encryptedString, password: password
|
||||
)
|
||||
#if DEBUG
|
||||
print("🖼️ PASS2 candidate[\(i)] decrypted \(data.count) bytes, prefix: \(data.prefix(20).map { String(format: "%02x", $0) }.joined())")
|
||||
#endif
|
||||
if let img = parseImageData(data) { return img }
|
||||
#if DEBUG
|
||||
print("🖼️ PASS2 candidate[\(i)] parseImageData FAILED")
|
||||
#endif
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("🖼️ PASS2 candidate[\(i)] FAILED: \(error)")
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -125,7 +125,7 @@ struct MessageCellView: View, Equatable {
|
||||
items: contextMenuItems(for: message),
|
||||
previewShape: MessageBubbleShape(position: position, outgoing: outgoing),
|
||||
isOutgoing: outgoing,
|
||||
replyQuoteHeight: replyData != nil ? 46 : 0,
|
||||
replyQuoteHeight: replyData != nil ? 49 : 0,
|
||||
onReplyQuoteTap: replyData.map { reply in
|
||||
{ [reply] in actions.onScrollToMessage(reply.message_id) }
|
||||
}
|
||||
@@ -154,21 +154,6 @@ struct MessageCellView: View, Equatable {
|
||||
let hasCaption = !reply.message.trimmingCharacters(in: .whitespaces).isEmpty
|
||||
&& !Self.isGarbageText(reply.message)
|
||||
|
||||
#if DEBUG
|
||||
let _ = {
|
||||
if reply.attachments.isEmpty {
|
||||
print("⚠️ Forward bubble: reply has NO attachments. message_id=\(reply.message_id), text='\(reply.message.prefix(50))', publicKey=\(reply.publicKey.prefix(12))")
|
||||
if let att = message.attachments.first(where: { $0.type == .messages }) {
|
||||
let blobPrefix = att.blob.prefix(60)
|
||||
let isEncrypted = att.blob.contains(":") && !att.blob.hasPrefix("[")
|
||||
print("⚠️ raw .messages blob (\(att.blob.count) chars): '\(blobPrefix)...' encrypted=\(isEncrypted)")
|
||||
}
|
||||
} else {
|
||||
print("📋 Forward bubble: message_id=\(reply.message_id.prefix(16)), \(reply.attachments.count) atts (images=\(imageAttachments.count), files=\(fileAttachments.count)), caption=\(hasCaption)")
|
||||
}
|
||||
}()
|
||||
#endif
|
||||
|
||||
let fallbackText: String = {
|
||||
if hasCaption { return reply.message }
|
||||
if !imageAttachments.isEmpty { return hasVisualAttachments ? "" : "Photo" }
|
||||
@@ -561,7 +546,7 @@ struct MessageCellView: View, Equatable {
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
|
||||
return "Attachment"
|
||||
}()
|
||||
let accentColor = outgoing ? Color.white.opacity(0.5) : RosettaColors.figmaBlue
|
||||
let accentColor = outgoing ? Color.white : RosettaColors.figmaBlue
|
||||
let imageAttachment = reply.attachments.first(where: { $0.type == 0 })
|
||||
let blurHash: String? = {
|
||||
guard let att = imageAttachment, !att.preview.isEmpty else { return nil }
|
||||
@@ -570,40 +555,37 @@ struct MessageCellView: View, Equatable {
|
||||
}()
|
||||
|
||||
HStack(spacing: 0) {
|
||||
RoundedRectangle(cornerRadius: 1.5)
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(accentColor)
|
||||
.frame(width: 3)
|
||||
.padding(.vertical, 4)
|
||||
|
||||
if let att = imageAttachment {
|
||||
ReplyQuoteThumbnail(attachment: att, blurHash: blurHash)
|
||||
.padding(.leading, 6)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 1) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(senderName)
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.tracking(-0.23)
|
||||
.foregroundStyle(outgoing ? Color.white.opacity(0.85) : RosettaColors.figmaBlue)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(outgoing ? Color.white : RosettaColors.figmaBlue)
|
||||
.lineLimit(1)
|
||||
Text(previewText)
|
||||
.font(.system(size: 15, weight: .regular))
|
||||
.tracking(-0.23)
|
||||
.foregroundStyle(outgoing ? Color.white.opacity(0.7) : RosettaColors.Adaptive.textSecondary)
|
||||
.font(.system(size: 14, weight: .regular))
|
||||
.foregroundStyle(outgoing ? Color.white : RosettaColors.Adaptive.text)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.leading, 6)
|
||||
.padding(.leading, 8)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.frame(height: 41)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(outgoing ? Color.white.opacity(0.08) : Color.white.opacity(0.06))
|
||||
.fill(outgoing ? Color.white.opacity(0.12) : RosettaColors.figmaBlue.opacity(0.12))
|
||||
)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 0)
|
||||
.padding(.bottom, 2)
|
||||
}
|
||||
|
||||
// MARK: - Forwarded File Preview
|
||||
|
||||
@@ -253,17 +253,11 @@ struct MessageImageView: View {
|
||||
|
||||
let tag = attachment.effectiveDownloadTag
|
||||
guard !tag.isEmpty else {
|
||||
#if DEBUG
|
||||
print("📸 DOWNLOAD FAIL: empty tag for attachment \(attachment.id), preview=\(attachment.preview.prefix(60)), transportTag=\(attachment.transportTag)")
|
||||
#endif
|
||||
downloadError = true
|
||||
return
|
||||
}
|
||||
|
||||
guard let storedPassword = message.attachmentPassword, !storedPassword.isEmpty else {
|
||||
#if DEBUG
|
||||
print("📸 DOWNLOAD FAIL: nil/empty attachmentPassword for msgId=\(message.id.prefix(8))… attId=\(attachment.id)")
|
||||
#endif
|
||||
downloadError = true
|
||||
return
|
||||
}
|
||||
@@ -272,24 +266,12 @@ struct MessageImageView: View {
|
||||
downloadError = false
|
||||
|
||||
let server = attachment.transportServer
|
||||
#if DEBUG
|
||||
print("📸 DOWNLOAD START: tag=\(tag) server=\(server) storedPwd=\(storedPassword.prefix(30))… attId=\(attachment.id)")
|
||||
#endif
|
||||
Task {
|
||||
do {
|
||||
let encryptedData = try await TransportManager.shared.downloadFile(tag: tag, server: server)
|
||||
let encryptedString = String(decoding: encryptedData, as: UTF8.self)
|
||||
|
||||
#if DEBUG
|
||||
let colonIdx = encryptedString.firstIndex(of: ":")
|
||||
let hasColonSeparator = colonIdx != nil
|
||||
print("📸 DOWNLOADED: \(encryptedData.count) bytes, hasColon=\(hasColonSeparator), first50=\(encryptedString.prefix(50))")
|
||||
#endif
|
||||
|
||||
let passwords = MessageCrypto.attachmentPasswordCandidates(from: storedPassword)
|
||||
#if DEBUG
|
||||
print("📸 CANDIDATES (\(passwords.count)): \(passwords.map { "\($0.prefix(20))…(\($0.count)ch)" })")
|
||||
#endif
|
||||
let downloadedImage = decryptAndParseImage(
|
||||
encryptedString: encryptedString, passwords: passwords
|
||||
)
|
||||
@@ -298,21 +280,12 @@ struct MessageImageView: View {
|
||||
if let downloadedImage {
|
||||
image = downloadedImage
|
||||
AttachmentCache.shared.saveImage(downloadedImage, forAttachmentId: attachment.id)
|
||||
#if DEBUG
|
||||
print("📸 DECRYPT OK: attId=\(attachment.id) imageSize=\(downloadedImage.size)")
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
print("📸 DECRYPT FAIL: all \(passwords.count) candidates failed for attId=\(attachment.id)")
|
||||
#endif
|
||||
downloadError = true
|
||||
}
|
||||
isDownloading = false
|
||||
}
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("📸 DOWNLOAD ERROR: \(error) for tag=\(tag)")
|
||||
#endif
|
||||
await MainActor.run {
|
||||
downloadError = true
|
||||
isDownloading = false
|
||||
@@ -323,40 +296,24 @@ struct MessageImageView: View {
|
||||
|
||||
private func decryptAndParseImage(encryptedString: String, passwords: [String]) -> UIImage? {
|
||||
let crypto = CryptoManager.shared
|
||||
for (i, password) in passwords.enumerated() {
|
||||
for password in passwords {
|
||||
do {
|
||||
let data = try crypto.decryptWithPassword(
|
||||
encryptedString, password: password, requireCompression: true
|
||||
)
|
||||
#if DEBUG
|
||||
print("📸 PASS1 candidate[\(i)] decrypted \(data.count) bytes")
|
||||
#endif
|
||||
if let img = parseImageData(data) { return img }
|
||||
#if DEBUG
|
||||
print("📸 PASS1 candidate[\(i)] parseImageData FAILED (data prefix: \(data.prefix(30).map { String(format: "%02x", $0) }.joined()))")
|
||||
#endif
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("📸 PASS1 candidate[\(i)] decrypt FAILED: \(error)")
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
}
|
||||
for (i, password) in passwords.enumerated() {
|
||||
for password in passwords {
|
||||
do {
|
||||
let data = try crypto.decryptWithPassword(
|
||||
encryptedString, password: password
|
||||
)
|
||||
#if DEBUG
|
||||
print("📸 PASS2 candidate[\(i)] decrypted \(data.count) bytes, prefix: \(data.prefix(20).map { String(format: "%02x", $0) }.joined())")
|
||||
#endif
|
||||
if let img = parseImageData(data) { return img }
|
||||
#if DEBUG
|
||||
print("📸 PASS2 candidate[\(i)] parseImageData FAILED")
|
||||
#endif
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("📸 PASS2 candidate[\(i)] decrypt FAILED: \(error)")
|
||||
#endif
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -244,7 +244,9 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
bubbleView.addSubview(clockFrameView)
|
||||
bubbleView.addSubview(clockMinView)
|
||||
|
||||
// Reply quote
|
||||
// Reply quote — Telegram parity: accent-tinted bg + 4pt radius bar
|
||||
replyContainer.layer.cornerRadius = 4.0
|
||||
replyContainer.clipsToBounds = true
|
||||
replyBar.layer.cornerRadius = 4.0
|
||||
replyContainer.addSubview(replyBar)
|
||||
replyNameLabel.font = Self.replyNameFont
|
||||
@@ -519,16 +521,17 @@ final class NativeMessageCell: UICollectionViewCell {
|
||||
// Bubble color (bubbleLayer is shadow-only; fill comes from bubbleImageView)
|
||||
photoContainer.backgroundColor = isOutgoing ? Self.outgoingColor : Self.incomingColor
|
||||
|
||||
// Reply quote
|
||||
// Reply quote — Telegram parity colors
|
||||
if let replyName {
|
||||
replyContainer.isHidden = false
|
||||
replyContainer.backgroundColor = isOutgoing
|
||||
? UIColor.white.withAlphaComponent(0.12)
|
||||
: Self.outgoingColor.withAlphaComponent(0.12)
|
||||
replyBar.backgroundColor = isOutgoing ? .white : Self.outgoingColor
|
||||
replyNameLabel.text = replyName
|
||||
replyNameLabel.textColor = isOutgoing ? .white : Self.outgoingColor
|
||||
replyTextLabel.text = replyText ?? ""
|
||||
replyTextLabel.textColor = isOutgoing
|
||||
? UIColor.white.withAlphaComponent(0.8)
|
||||
: UIColor.white.withAlphaComponent(0.6)
|
||||
replyTextLabel.textColor = .white
|
||||
} else {
|
||||
replyContainer.isHidden = true
|
||||
}
|
||||
|
||||
@@ -614,12 +614,9 @@ final class NativeMessageListController: UIViewController {
|
||||
|
||||
// Build date for each visible cell, collect section ranges.
|
||||
var sectionMap: [String: (topY: CGFloat, bottomY: CGFloat)] = [:]
|
||||
let calendar = Calendar.current
|
||||
let now = Date()
|
||||
|
||||
for cell in collectionView.visibleCells {
|
||||
guard let nativeCell = cell as? NativeMessageCell,
|
||||
let layout = nativeCell.currentLayout else { continue }
|
||||
guard let nativeCell = cell as? NativeMessageCell else { continue }
|
||||
// Determine this cell's date text
|
||||
// Use the layout's dateHeaderText if available, else compute from message
|
||||
let cellFrame = collectionView.convert(cell.frame, to: view)
|
||||
@@ -696,16 +693,6 @@ final class NativeMessageListController: UIViewController {
|
||||
datePillPool[i].container.isHidden = true
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
if !sections.isEmpty {
|
||||
let desc = sections.enumerated().map { i, s in
|
||||
let y = min(max(s.topY + 6, stickyY), s.bottomY - pillH)
|
||||
return "\(s.text)@\(Int(y))[\(Int(s.topY))→\(Int(s.bottomY))]"
|
||||
}.joined(separator: " | ")
|
||||
print("📅 pills=\(usedPillCount) \(desc)")
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. Show/hide with timer.
|
||||
if usedPillCount > 0 {
|
||||
showDatePills()
|
||||
@@ -937,10 +924,6 @@ final class NativeMessageListController: UIViewController {
|
||||
textLayoutCache.removeAll()
|
||||
return
|
||||
}
|
||||
#if DEBUG
|
||||
let start = CFAbsoluteTimeGetCurrent()
|
||||
#endif
|
||||
|
||||
let (layouts, textLayouts) = MessageCellLayout.batchCalculate(
|
||||
messages: messages,
|
||||
maxBubbleWidth: config.maxBubbleWidth,
|
||||
@@ -950,11 +933,6 @@ final class NativeMessageListController: UIViewController {
|
||||
)
|
||||
layoutCache = layouts
|
||||
textLayoutCache = textLayouts
|
||||
|
||||
#if DEBUG
|
||||
let elapsed = (CFAbsoluteTimeGetCurrent() - start) * 1000
|
||||
print("⚡ PERF_LAYOUT | \(messages.count) msgs | \(String(format: "%.1f", elapsed))ms | textLayouts cached: \(textLayouts.count)")
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Inset Management
|
||||
@@ -1095,12 +1073,6 @@ final class NativeMessageListController: UIViewController {
|
||||
let endFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
|
||||
else { return }
|
||||
|
||||
#if DEBUG
|
||||
let screenH = view.window?.screen.bounds.height ?? UIScreen.main.bounds.height
|
||||
let kbH = max(0, screenH - endFrame.minY)
|
||||
print("⌨️ keyboardWillChange | kbHeight=\(kbH) endFrame=\(endFrame) composerH=\(lastComposerHeight) currentInset=\(collectionView?.contentInset.top ?? 0)")
|
||||
#endif
|
||||
|
||||
let screenHeight = view.window?.screen.bounds.height ?? UIScreen.main.bounds.height
|
||||
let keyboardHeight = max(0, screenHeight - endFrame.minY)
|
||||
let safeBottom = view.safeAreaInsets.bottom
|
||||
@@ -1173,9 +1145,6 @@ final class NativeMessageListController: UIViewController {
|
||||
}
|
||||
|
||||
@objc private func keyboardDidHide() {
|
||||
#if DEBUG
|
||||
print("⌨️ didHide | cv.frame=\(collectionView?.frame ?? .zero) composer.frame=\(composerView?.frame ?? .zero) offset=\(collectionView?.contentOffset ?? .zero) composerConst=\(composerBottomConstraint?.constant ?? 0)")
|
||||
#endif
|
||||
currentKeyboardHeight = 0
|
||||
isKeyboardAnimating = false
|
||||
onKeyboardDidHide?()
|
||||
|
||||
@@ -8,12 +8,7 @@ struct SearchView: View {
|
||||
@State private var searchText = ""
|
||||
@State private var navigationPath: [ChatRoute] = []
|
||||
|
||||
@MainActor static var _bodyCount = 0
|
||||
var body: some View {
|
||||
#if DEBUG
|
||||
let _ = Self._bodyCount += 1
|
||||
let _ = print("🔵 SearchView.body #\(Self._bodyCount)")
|
||||
#endif
|
||||
NavigationStack(path: $navigationPath) {
|
||||
ZStack(alignment: .bottom) {
|
||||
RosettaColors.Adaptive.background
|
||||
@@ -142,13 +137,8 @@ private extension SearchView {
|
||||
/// does NOT propagate to `SearchView`'s NavigationStack.
|
||||
private struct FavoriteContactsRow: View {
|
||||
@Binding var navigationPath: [ChatRoute]
|
||||
@MainActor static var _bodyCount = 0
|
||||
|
||||
var body: some View {
|
||||
#if DEBUG
|
||||
let _ = Self._bodyCount += 1
|
||||
let _ = print("🟠 FavoriteContactsRow.body #\(Self._bodyCount)")
|
||||
#endif
|
||||
let dialogs = DialogRepository.shared.sortedDialogs.prefix(10)
|
||||
if !dialogs.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
@@ -192,13 +182,7 @@ private struct FavoriteContactsRow: View {
|
||||
private struct RecentSection: View {
|
||||
@ObservedObject var viewModel: SearchViewModel
|
||||
@Binding var navigationPath: [ChatRoute]
|
||||
@MainActor static var _bodyCount = 0
|
||||
|
||||
var body: some View {
|
||||
#if DEBUG
|
||||
let _ = Self._bodyCount += 1
|
||||
let _ = print("🟤 RecentSection.body #\(Self._bodyCount)")
|
||||
#endif
|
||||
if viewModel.recentSearches.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user