mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
feat: verification seal on private message sender rows (#1573)
* feat: show verification seal on private message sender rows DM-only filled seal next to verified peers' names closes the remaining UI gap from #1439 without dressing public mesh timelines. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: skip in-string verified ✓ on private rows with SF Symbol seal Co-authored-by: Cursor <cursoragent@cursor.com> * Add verified_sender catalog entry (30 locales) + live seal repaint on verify The accessibility label existed only in code, so VoiceOver read english in 29 locales; the coverage test can't see source-only keys. Also forward peerIdentityStore.$verifiedFingerprints into objectWillChange so toggling verification repaints rows in an open DM instead of waiting for the next unrelated invalidation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
681c180060
commit
0152344554
@ -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() {
|
||||
|
||||
@ -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" : {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user