mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:25:20 +00:00
* Make peer lists accessible and actionable; block by stable identity Who you can reach — the app's most important fact — was encoded in unlabeled 10pt icons with macOS-only tooltips, and the mesh list had no actions (block/favorite/verify were slash-command-only). - Both peer lists become real accessibility citizens: each row is one element announcing name, reachability, and favorite/unread/blocked state, with a button trait and custom actions for the gesture-only interactions. Neither file previously had a single accessibility modifier. Reachability icons gain tooltips reusing existing strings; teleported vs in-area pins are explained. - Mesh rows gain the context menu the geohash list already had: direct message, favorite, show fingerprint, block/unblock. The fingerprint double-tap, previously shadowed by the single tap, is reordered so it fires. - The DM header's offline state (previously EmptyView — absence of a glyph as the only signal) becomes a dimmed "offline" tag, and a geohash DM — always Nostr-routed — no longer mislabels itself "offline". - App Info gains a SYMBOLS legend defining every glyph the lists and headers use; nothing defined them before. - Mesh block/unblock now resolve by the peer's stable Noise identity instead of a `/block <displayName>` string, so the exact tapped row is affected and offline peers can be unblocked (with covering tests). New strings are added source-language (en) only. * Surface block/unblock feedback in the conversation where it was triggered setMeshPeerBlocked silently returned when the peer's identity could not be resolved (e.g. long-press-blocking an old public message from a sender who left and was never a favorite), where the /block command printed "cannot block X: not found or unable to verify identity" — post that same message from the guard branch. Both the failure and confirmation messages now route through addCommandOutput instead of addSystemMessage, so blocking from inside a private chat prints into that chat rather than invisibly into the public timeline (same routing #1363 applied to command output). The confirmation also reuses the /block wording ("blocked X. you will no longer receive messages from them") for parity with the command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove dead accessibility label and unreachable /unblock fallback The favorite button's .accessibilityLabel in MeshPeerList is unreachable: the row-level .accessibilityElement(children: .ignore) swallows child elements, and the row's custom accessibility action already covers favoriting. ConversationUIModel.unblock is only called from the mesh peer list with a non-optional mesh peerID, so the "/unblock <name>" fallback branch could never run — take PeerID directly and drop the branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com> Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
185 lines
5.9 KiB
Swift
185 lines
5.9 KiB
Swift
//
|
|
// UnifiedPeerServiceTests.swift
|
|
// bitchatTests
|
|
//
|
|
// Tests for UnifiedPeerService fingerprint and block resolution.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
import BitFoundation
|
|
@testable import bitchat
|
|
|
|
struct UnifiedPeerServiceTests {
|
|
|
|
@Test @MainActor
|
|
func getFingerprint_prefersMeshService() async {
|
|
let transport = MockTransport()
|
|
let identity = TestIdentityManager()
|
|
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
|
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
|
|
|
|
let peerID = PeerID(str: "00000000000000CC")
|
|
transport.peerFingerprints[peerID] = "fp-1"
|
|
|
|
let fingerprint = service.getFingerprint(for: peerID)
|
|
|
|
#expect(fingerprint == "fp-1")
|
|
}
|
|
|
|
@Test @MainActor
|
|
func isBlocked_usesSocialIdentity() async {
|
|
let transport = MockTransport()
|
|
let identity = TestIdentityManager()
|
|
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
|
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
|
|
|
|
let peerID = PeerID(str: "00000000000000DD")
|
|
let fingerprint = "fp-blocked"
|
|
transport.peerFingerprints[peerID] = fingerprint
|
|
identity.setBlocked(fingerprint, isBlocked: true)
|
|
|
|
#expect(service.isBlocked(peerID))
|
|
}
|
|
|
|
@Test @MainActor
|
|
func setBlocked_persistsByFingerprintAndToggles() async {
|
|
let transport = MockTransport()
|
|
let identity = TestIdentityManager()
|
|
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
|
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
|
|
|
|
let peerID = PeerID(str: "00000000000000EE")
|
|
let fingerprint = "fp-target"
|
|
transport.peerFingerprints[peerID] = fingerprint
|
|
|
|
// Blocking resolves and persists by the peer's fingerprint.
|
|
let resolved = service.setBlocked(peerID, blocked: true)
|
|
#expect(resolved == fingerprint)
|
|
#expect(identity.isBlocked(fingerprint: fingerprint))
|
|
#expect(service.isBlocked(peerID))
|
|
|
|
// Unblocking clears it against the same identity.
|
|
let unresolved = service.setBlocked(peerID, blocked: false)
|
|
#expect(unresolved == fingerprint)
|
|
#expect(!identity.isBlocked(fingerprint: fingerprint))
|
|
#expect(!service.isBlocked(peerID))
|
|
}
|
|
|
|
@Test @MainActor
|
|
func setBlocked_unknownIdentityReturnsNil() async {
|
|
let transport = MockTransport()
|
|
let identity = TestIdentityManager()
|
|
let idBridge = NostrIdentityBridge(keychain: MockKeychainHelper())
|
|
let service = UnifiedPeerService(meshService: transport, idBridge: idBridge, identityManager: identity)
|
|
|
|
// No fingerprint resolvable for this peer (offline & unknown).
|
|
let peerID = PeerID(str: "00000000000000FF")
|
|
|
|
#expect(service.setBlocked(peerID, blocked: true) == nil)
|
|
#expect(!service.isBlocked(peerID))
|
|
}
|
|
}
|
|
|
|
private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
|
|
private var socialIdentities: [String: SocialIdentity] = [:]
|
|
private var favorites: Set<String> = []
|
|
private var blockedNostr: Set<String> = []
|
|
private var verified: Set<String> = []
|
|
|
|
func forceSave() {}
|
|
|
|
func getSocialIdentity(for fingerprint: String) -> SocialIdentity? {
|
|
socialIdentities[fingerprint]
|
|
}
|
|
|
|
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) {}
|
|
|
|
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] {
|
|
[]
|
|
}
|
|
|
|
func updateSocialIdentity(_ identity: SocialIdentity) {
|
|
socialIdentities[identity.fingerprint] = identity
|
|
}
|
|
|
|
func getFavorites() -> Set<String> {
|
|
favorites
|
|
}
|
|
|
|
func setFavorite(_ fingerprint: String, isFavorite: Bool) {
|
|
if isFavorite {
|
|
favorites.insert(fingerprint)
|
|
} else {
|
|
favorites.remove(fingerprint)
|
|
}
|
|
}
|
|
|
|
func isFavorite(fingerprint: String) -> Bool {
|
|
favorites.contains(fingerprint)
|
|
}
|
|
|
|
func isBlocked(fingerprint: String) -> Bool {
|
|
socialIdentities[fingerprint]?.isBlocked ?? false
|
|
}
|
|
|
|
func setBlocked(_ fingerprint: String, isBlocked: Bool) {
|
|
var identity = socialIdentities[fingerprint] ?? SocialIdentity(
|
|
fingerprint: fingerprint,
|
|
localPetname: nil,
|
|
claimedNickname: "",
|
|
trustLevel: .unknown,
|
|
isFavorite: false,
|
|
isBlocked: false,
|
|
notes: nil
|
|
)
|
|
identity.isBlocked = isBlocked
|
|
socialIdentities[fingerprint] = identity
|
|
}
|
|
|
|
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
|
|
blockedNostr.contains(pubkeyHexLowercased)
|
|
}
|
|
|
|
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {
|
|
if isBlocked {
|
|
blockedNostr.insert(pubkeyHexLowercased)
|
|
} else {
|
|
blockedNostr.remove(pubkeyHexLowercased)
|
|
}
|
|
}
|
|
|
|
func getBlockedNostrPubkeys() -> Set<String> {
|
|
blockedNostr
|
|
}
|
|
|
|
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
|
|
|
|
func updateHandshakeState(peerID: PeerID, state: HandshakeState) {}
|
|
|
|
func clearAllIdentityData() {
|
|
socialIdentities.removeAll()
|
|
favorites.removeAll()
|
|
blockedNostr.removeAll()
|
|
verified.removeAll()
|
|
}
|
|
|
|
func removeEphemeralSession(peerID: PeerID) {}
|
|
|
|
func setVerified(fingerprint: String, verified: Bool) {
|
|
if verified {
|
|
self.verified.insert(fingerprint)
|
|
} else {
|
|
self.verified.remove(fingerprint)
|
|
}
|
|
}
|
|
|
|
func isVerified(fingerprint: String) -> Bool {
|
|
verified.contains(fingerprint)
|
|
}
|
|
|
|
func getVerifiedFingerprints() -> Set<String> {
|
|
verified
|
|
}
|
|
}
|