diff --git a/bitchat/App/ConversationUIModel.swift b/bitchat/App/ConversationUIModel.swift index 91f0afef..53062d45 100644 --- a/bitchat/App/ConversationUIModel.swift +++ b/bitchat/App/ConversationUIModel.swift @@ -128,6 +128,18 @@ final class ConversationUIModel: ObservableObject { message.sender == currentNickname || message.senderPeerID == chatViewModel.meshService.myPeerID } + /// Whether a private-message row should show the filled verification seal + /// next to the sender name (#1439). Scoped to DMs only — public timelines + /// have different trust semantics and stay undressed. + func showsVerifiedSeal(for message: BitchatMessage) -> Bool { + guard message.isPrivate, + message.sender != "system", + !isSentByCurrentUser(message), + let peerID = message.senderPeerID else { return false } + guard let fingerprint = chatViewModel.getFingerprint(for: peerID) else { return false } + return chatViewModel.peerIdentityStore.isVerified(fingerprint) + } + func senderDisplayName(for peerID: PeerID, fallbackMessages: [BitchatMessage]) -> String? { if peerID.isGeoDM || peerID.isGeoChat { return chatViewModel.geohashDisplayName(for: peerID) @@ -219,6 +231,15 @@ final class ConversationUIModel: ObservableObject { self?.refreshComputedState() } .store(in: &cancellables) + + // Verify/unverify while a DM is open must repaint existing rows — + // showsVerifiedSeal is computed per render, so forward the store change. + chatViewModel.peerIdentityStore.$verifiedFingerprints + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) } private func refreshComputedState() { diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index c43da7a2..f869ab85 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -26973,6 +26973,191 @@ } } }, + "content.accessibility.verified_sender" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "مرسل موثق" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "যাচাইকৃত প্রেরক" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifizierter absender" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verified sender" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remitente verificado" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "فرستنده تأییدشده" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "Beripikadong nagpadala" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Expéditeur vérifié" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "שולח מאומת" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सत्यापित प्रेषक" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pengirim terverifikasi" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Mittente verificato" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "確認済みの送信者" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "확인된 보낸 사람" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pengirim yang disahkan" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "प्रमाणित प्रेषक" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Geverifieerde afzender" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zweryfikowany nadawca" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remetente verificado" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Remetente verificado" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Подтверждённый отправитель" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Verifierad avsändare" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "சரிபார்க்கப்பட்ட அனுப்புநர்" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ผู้ส่งที่ยืนยันแล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doğrulanmış gönderen" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Підтверджений відправник" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تصدیق شدہ مرسل" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Người gửi đã xác minh" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已验证的发送者" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已驗證的傳送者" + } + } + } + }, "content.accessibility.view_fingerprint_hint" : { "extractionState" : "manual", "localizations" : { diff --git a/bitchat/ViewModels/ChatMessageFormatter.swift b/bitchat/ViewModels/ChatMessageFormatter.swift index ae663f84..1a00315b 100644 --- a/bitchat/ViewModels/ChatMessageFormatter.swift +++ b/bitchat/ViewModels/ChatMessageFormatter.swift @@ -68,7 +68,10 @@ final class ChatMessageFormatter { suffixStyle.foregroundColor = baseColor.opacity(0.6) result.append(AttributedString(suffix).mergingAttributes(suffixStyle)) } - if isVerifiedSender { + // Private rows render a filled SF Symbol seal beside the lock + // (TextMessageView / MediaMessageView); skip the in-string ✓ there + // so verified DMs don't show two markers. + if isVerifiedSender, !message.isPrivate { appendVerifiedSeal(to: &result, baseColor: baseColor, design: design) } result.append(AttributedString("> ").mergingAttributes(senderStyle)) @@ -388,7 +391,7 @@ final class ChatMessageFormatter { suffixStyle.foregroundColor = baseColor.opacity(0.6) result.append(AttributedString(suffix).mergingAttributes(suffixStyle)) } - if isVerifiedSender { + if isVerifiedSender, !message.isPrivate { appendVerifiedSeal(to: &result, baseColor: baseColor, design: design) } result.append(AttributedString("> ").mergingAttributes(senderStyle)) diff --git a/bitchat/Views/Components/TextMessageView.swift b/bitchat/Views/Components/TextMessageView.swift index a16e3ac6..ed376acf 100644 --- a/bitchat/Views/Components/TextMessageView.swift +++ b/bitchat/Views/Components/TextMessageView.swift @@ -50,6 +50,15 @@ struct TextMessageView: View { .padding(.trailing, 4) .accessibilityHidden(true) } + if conversationUIModel.showsVerifiedSeal(for: message) { + Image(systemName: "checkmark.seal.fill") + .font(.bitchatSystem(size: 8)) + .foregroundColor(Color.green.opacity(0.85)) + .padding(.trailing, 4) + .accessibilityLabel( + String(localized: "content.accessibility.verified_sender", defaultValue: "Verified sender", comment: "Accessibility label for the seal next to a verified peer's name on a private message") + ) + } if message.isBridged { Image(systemName: "network") .font(.bitchatSystem(size: 8)) diff --git a/bitchat/Views/Media/MediaMessageView.swift b/bitchat/Views/Media/MediaMessageView.swift index 2e2d1429..29711f38 100644 --- a/bitchat/Views/Media/MediaMessageView.swift +++ b/bitchat/Views/Media/MediaMessageView.swift @@ -48,6 +48,15 @@ struct MediaMessageView: View { .padding(.trailing, 4) .accessibilityHidden(true) } + if conversationUIModel.showsVerifiedSeal(for: message) { + Image(systemName: "checkmark.seal.fill") + .font(.bitchatSystem(size: 8)) + .foregroundColor(Color.green.opacity(0.85)) + .padding(.trailing, 4) + .accessibilityLabel( + String(localized: "content.accessibility.verified_sender", defaultValue: "Verified sender", comment: "Accessibility label for the seal next to a verified peer's name on a private message") + ) + } VStack(alignment: .leading, spacing: 2) { HStack(alignment: .center, spacing: 4) { Text(conversationUIModel.formatMessageHeader(message, colorScheme: colorScheme, theme: theme))