diff --git a/bitchat/ViewModels/ChatMessageFormatter.swift b/bitchat/ViewModels/ChatMessageFormatter.swift index 62723d7a..a2259b08 100644 --- a/bitchat/ViewModels/ChatMessageFormatter.swift +++ b/bitchat/ViewModels/ChatMessageFormatter.swift @@ -41,7 +41,9 @@ final class ChatMessageFormatter { }() let isDark = colorScheme == .dark - if let cachedText = message.getCachedFormattedText(isDark: isDark, isSelf: isSelf, variant: theme.formatCacheVariant) { + let isVerifiedSender = !isSelf && isVerifiedSender(of: message) + let cacheVariant = theme.formatCacheVariant + (isVerifiedSender ? "-vf" : "") + if let cachedText = message.getCachedFormattedText(isDark: isDark, isSelf: isSelf, variant: cacheVariant) { return cachedText } @@ -66,6 +68,9 @@ final class ChatMessageFormatter { suffixStyle.foregroundColor = baseColor.opacity(0.6) result.append(AttributedString(suffix).mergingAttributes(suffixStyle)) } + if isVerifiedSender { + appendVerifiedSeal(to: &result, baseColor: baseColor, design: design) + } result.append(AttributedString("> ").mergingAttributes(senderStyle)) let content = message.content @@ -335,7 +340,7 @@ final class ChatMessageFormatter { result.append(timestamp.mergingAttributes(timestampStyle)) } - message.setCachedFormattedText(result, isDark: isDark, isSelf: isSelf, variant: theme.formatCacheVariant) + message.setCachedFormattedText(result, isDark: isDark, isSelf: isSelf, variant: cacheVariant) return result } @@ -356,6 +361,7 @@ final class ChatMessageFormatter { let isDark = colorScheme == .dark let baseColor: Color = isSelf ? .orange : peerColor(for: message, isDark: isDark) + let isVerifiedSender = !isSelf && isVerifiedSender(of: message) if message.sender == "system" { var style = AttributeContainer() @@ -381,6 +387,9 @@ final class ChatMessageFormatter { suffixStyle.foregroundColor = baseColor.opacity(0.6) result.append(AttributedString(suffix).mergingAttributes(suffixStyle)) } + if isVerifiedSender { + appendVerifiedSeal(to: &result, baseColor: baseColor, design: design) + } result.append(AttributedString("> ").mergingAttributes(senderStyle)) return result } @@ -427,6 +436,29 @@ final class ChatMessageFormatter { } private extension ChatMessageFormatter { + /// Whether the message sender has a fingerprint the user has verified. + /// Used for the in-chat seal next to `<@name>` so verification is visible + /// without opening the fingerprint sheet (#1439). + func isVerifiedSender(of message: BitchatMessage) -> Bool { + guard let peerID = message.senderPeerID, + let fingerprint = viewModel.getFingerprint(for: peerID) else { + return false + } + return viewModel.peerIdentityStore.isVerified(fingerprint) + } + + func appendVerifiedSeal( + to result: inout AttributedString, + baseColor: Color, + design: Font.Design + ) { + var sealStyle = AttributeContainer() + // Match the peer-list verified seal: filled checkmark in the sender tint. + sealStyle.foregroundColor = baseColor + sealStyle.font = .bitchatSystem(size: 11, weight: .semibold, design: design) + result.append(AttributedString(" ✓").mergingAttributes(sealStyle)) + } + func peerColor(for message: BitchatMessage, isDark: Bool) -> Color { if let spid = message.senderPeerID { if spid.isGeoChat || spid.isGeoDM {