diff --git a/bitchat/App/PeerListModel.swift b/bitchat/App/PeerListModel.swift index 649d99b6..0bb74966 100644 --- a/bitchat/App/PeerListModel.swift +++ b/bitchat/App/PeerListModel.swift @@ -167,6 +167,16 @@ final class PeerListModel: ObservableObject { } .store(in: &cancellables) + // New/updated messages in an existing thread must refresh ordering and + // preview while the people sheet is open — conversationIDs alone does + // not change for those mutations. + conversations.changes + .debounce(for: .milliseconds(120), scheduler: DispatchQueue.main) + .sink { [weak self] _ in + self?.refresh() + } + .store(in: &cancellables) + chatViewModel.groupStore.$groups .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -274,7 +284,9 @@ final class PeerListModel: ObservableObject { self.groupRows = groupRows renderID = ( recentDirectRows.map { - "recent:\($0.id)-\($0.hasUnread)-\($0.displayName)-\($0.preview)" + // Hash the preview so a `|` inside message text cannot collide + // with the render-id separator and skip a re-render. + "recent:\($0.id)-\($0.hasUnread)-\($0.displayName)-\($0.preview.stableRenderHash)" } + meshRows.map { "\($0.id)-\($0.displayName)-\($0.isConnected)-\($0.isReachable)-\($0.hasUnread)-\($0.isFavorite)-\($0.isBlocked)" @@ -295,25 +307,32 @@ final class PeerListModel: ObservableObject { guard peerID != myPeerID else { return nil } // Already in that thread — no need to list it again here. guard peerID != selected else { return nil } + // Match mesh rows: blocked people stay out of the people sheet. + guard !chatViewModel.isPeerBlocked(peerID) else { return nil } let messages = messagesByPeer[peerID] ?? [] guard let last = messages.last else { return nil } - let preview = last.content - .trimmingCharacters(in: .whitespacesAndNewlines) - let clipped: String - if preview.count > 48 { - clipped = String(preview.prefix(45)) + "…" - } else { - clipped = preview - } + let preview = Self.friendlyRecentPreview(last.content) return RecentDirectRow( peerID: peerID, displayName: chatViewModel.nicknameForPeer(peerID), hasUnread: chatViewModel.hasUnreadMessages(for: peerID), - preview: clipped + preview: preview ) } } + /// Strip media placeholder filenames (`[image] .jpg`) down to a + /// short type marker so the recent-DM preview stays readable. + private static func friendlyRecentPreview(_ content: String) -> String { + let trimmed = content.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.hasPrefix("[image]") { return "[image]" } + if trimmed.hasPrefix("[voice]") { return "[voice]" } + if trimmed.count > 48 { + return String(trimmed.prefix(45)) + "…" + } + return trimmed + } + private func buildGroupRows() -> [GroupChatRow] { let myFingerprint = chatViewModel.meshService.noiseIdentityFingerprint() return chatViewModel.groupStore.groups.map { group in @@ -352,3 +371,15 @@ final class PeerListModel: ObservableObject { return identity.publicKeyHex.lowercased() } } + +private extension String { + /// Stable within-process digest for render-id segments — avoids embedding + /// raw message text (and `|` separators) in the joined render token. + var stableRenderHash: String { + var hash: UInt64 = 5381 + for byte in utf8 { + hash = ((hash << 5) &+ hash) &+ UInt64(byte) + } + return String(hash, radix: 16) + } +} diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index 67b9e841..3e72e23c 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -39029,25 +39029,24 @@ } }, "content.people.recent_messages" : { - "comment" : "People sheet section header for recent private message threads", "extractionState" : "manual", "localizations" : { "ar" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "الرسائل الأخيرة" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "সাম্প্রতিক বার্তা" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "letzte nachrichten" } }, "en" : { @@ -39059,157 +39058,157 @@ "es" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "mensajes recientes" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "پیام‌های اخیر" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "mga kamakailang mensahe" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "messages récents" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "הודעות אחרונות" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "हाल के संदेश" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "pesan terbaru" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "messaggi recenti" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "最近のメッセージ" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "최근 메시지" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "mesej terbaru" } }, "ne" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "हालका सन्देशहरू" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "recente berichten" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "ostatnie wiadomości" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "mensagens recentes" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "mensagens recentes" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "недавние сообщения" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "senaste meddelanden" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "சமீபத்திய செய்திகள்" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "ข้อความล่าสุด" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "son mesajlar" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "нещодавні повідомлення" } }, "ur" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "حالیہ پیغامات" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "tin nhắn gần đây" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "最近的消息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "recent messages" + "value" : "最近的訊息" } } } diff --git a/bitchat/Views/RecentDirectMessagesList.swift b/bitchat/Views/RecentDirectMessagesList.swift index 5a996011..e1dd275c 100644 --- a/bitchat/Views/RecentDirectMessagesList.swift +++ b/bitchat/Views/RecentDirectMessagesList.swift @@ -1,4 +1,5 @@ import SwiftUI +import BitFoundation /// Compact recent-DM rows for the people sheet — reopen past private threads /// without hunting through the full mesh / favorites lists (#615). @@ -49,7 +50,11 @@ struct RecentDirectMessagesList: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .accessibilityLabel(row.displayName) + .accessibilityLabel( + row.hasUnread + ? "\(row.displayName), \(Strings.unread)" + : row.displayName + ) .accessibilityHint(Strings.openDMHint) } }