Remove dead code found by full Periphery audit; add scan config + advisory CI (#1410)

Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so
platform-specific code is never touched), with test targets indexed and
the share extension built. 277 dead declarations removed or demoted:
dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed-
feature remnants (autocomplete command suggestions, back-swipe tuning,
MediaSendError, GeohashParticipantTracker), unused Tor dormancy
bindings, assign-only properties, unused parameters (renamed to _), and
redundant public accessibility. 13 orphaned localization keys deleted
across all 29 locales (old pre-#1392 location-notes UI, app_info
warnings).

Two real tests were flagged as unused because they never ran: Swift
Testing methods missing @Test (NostrProtocolTests.
testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests.
testAssemblesCompressedLargeFrame). Re-armed both; they pass.

Deliberately kept, now recorded in .periphery.baseline.json: iOS-only
code invisible to the CI macOS scan, C FFI signatures, keep-alive
NWPathMonitor reference, InboundEventKey.eventID (dedup semantics),
wifiBulk capability bit (reserved for Wi-Fi bulk work, used by
BitFoundation package tests), and the String secureClear cluster
(exercised by package tests).

New: .periphery.yml config and an advisory Dead Code CI job (mirrors
the SwiftLint precedent from #1361) that fails on findings not in the
committed baseline.

Verified: full macOS app suite, BitFoundation (119) and BitLogger (13)
package tests green; periphery scan --strict exits clean.

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-08 11:24:19 +02:00
committed by GitHub
co-authored by jack Claude Fable 5
parent d5cf64f99e
commit ef848857b7
102 changed files with 137 additions and 3712 deletions
+30
View File
@@ -0,0 +1,30 @@
name: Dead Code
on:
push:
branches:
- main
pull_request:
jobs:
periphery:
name: Periphery scan
runs-on: macos-latest
timeout-minutes: 30
# Advisory, like SwiftLint (#1361): findings annotate the PR but don't
# block merges. Drop continue-on-error once the baseline proves stable.
continue-on-error: true
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Install Periphery
# homebrew-core formula; the peripheryapp tap lags years behind.
run: brew install periphery
- name: Scan for dead code
# Config comes from .periphery.yml; known findings (mostly iOS-only
# code invisible to a macOS scan) are suppressed by the committed
# baseline. --strict fails the step when NEW dead code appears.
run: periphery scan --strict --disable-update-check
+1
View File
@@ -0,0 +1 @@
{"v1":{"usrs":["param-buf-arti_bootstrap_summary(_:_:)-s:3Tor22arti_bootstrap_summary33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSpys4Int8VG_AEtF","param-dataDir-arti_start(_:_:)-s:3Tor10arti_start33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSPys4Int8VG_s6UInt16VtF","param-len-arti_bootstrap_summary(_:_:)-s:3Tor22arti_bootstrap_summary33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSpys4Int8VG_AEtF","param-socksPort-arti_start(_:_:)-s:3Tor10arti_start33_954FD7701B4E47ABB5F166D1CF862DC9LLys5Int32VSPys4Int8VG_s6UInt16VtF","s:13BitFoundation16PeerCapabilitiesV8wifiBulkACvpZ","s:13BitFoundation18KeychainReadResultO18isRecoverableErrorSbvp","s:13BitFoundation23KeychainManagerProtocolP11secureClearyySSzF","s:18bitchatTests_macOS12MockKeychainC11secureClearyySSzF","s:18bitchatTests_macOS20TrackingMockKeychainC11resetCountsyyF","s:18bitchatTests_macOS20TrackingMockKeychainC11secureClearyySSzF","s:18bitchatTests_macOS20TrackingMockKeychainC25totalSecureClearCallCountSivp","s:18bitchatTests_macOS20TrackingMockKeychainC26secureClearStringCallCountSivp","s:18bitchatTests_macOS20TrackingMockKeychainC27_secureClearStringCallCount06_AB6D1M24FD239F2969C82F4108818260LLSivp","s:18bitchatTests_macOS24FailingCacheSaveKeychain33_22380C7A11A569A0B83FA83F34C498A7LLC11secureClearyySSzF","s:18bitchatTests_macOS24MockGeohashPresenceTimer33_483587EFB96650EE130EFB09BBA2A1AALLC7handleryycvp","s:3Tor0A7ManagerC21goDormantOnBackgroundyyF","s:7bitchat10AppRuntimeC24handleScreenshotCaptured33_C8B369AD8BC1D9963A50CEDA77A4332ALLyyF","s:7bitchat10AppRuntimeC33handleDidBecomeActiveNotificationyyF","s:7bitchat10BLEServiceC18logBluetoothStatus33_69191C53E68500C17D98DBCF2BDA7100LLyySSF","s:7bitchat10BLEServiceC20centralRestorationID33_69191C53E68500C17D98DBCF2BDA7100LLSSvpZ","s:7bitchat10BLEServiceC22captureBluetoothStatus33_69191C53E68500C17D98DBCF2BDA7100LL7contextySS_tF","s:7bitchat10BLEServiceC23peripheralRestorationID33_69191C53E68500C17D98DBCF2BDA7100LLSSvpZ","s:7bitchat10BLEServiceC29scheduleBluetoothStatusSample33_69191C53E68500C17D98DBCF2BDA7100LL5after7contextySd_SStF","s:7bitchat10QRScanViewV8isActiveSbvp","s:7bitchat15BLEPeerRegistryV5countSivp","s:7bitchat15KeychainManagerC11secureClearyySSzF","s:7bitchat15PaymentChipViewV7openURL33_10AC50641B1EBCD52E5092A2E521D236LL7SwiftUI13OpenURLActionVvp","s:7bitchat15TransportConfigO29uiBatchDispatchStaggerSecondsSdvpZ","s:7bitchat15TransportConfigO35uiShareExtensionDismissDelaySecondsSdvpZ","s:7bitchat15TransportConfigO38bleBackgroundPendingConnectSlotReserveSivpZ","s:7bitchat17GossipSyncManagerC10persistNowyyF","s:7bitchat17NostrRelayManagerC15InboundEventKey33_E4160FE8A9A2C9D6308EAAD5A8B5CB07LLV7eventIDSSvp","s:7bitchat25LocationNotesDependenciesV3now10Foundation4DateVycvp","s:7bitchat25NWPathReachabilityMonitorC7monitor33_84633C9DBCAF57538179C1E04DB8E015LL7Network0bD0CSgvp"]}}
+14
View File
@@ -0,0 +1,14 @@
# Periphery dead-code scan configuration (https://github.com/peripheryapp/periphery)
#
# CI runs the macOS scheme only (an iOS scan needs a device destination and
# doubles the build time). macOS-only scans falsely flag iOS-only code —
# state restoration, screenshot handlers, background BLE sampling — so those
# findings live in .periphery.baseline.json rather than being "fixed".
# When auditing by hand, scan BOTH schemes and intersect:
# periphery scan --schemes "bitchat (iOS)" -- -destination 'generic/platform=iOS' ARCHS=arm64
project: bitchat.xcodeproj
schemes:
- bitchat (macOS)
retain_swift_ui_previews: true
relative_results: true
baseline: .periphery.baseline.json
-22
View File
@@ -36,34 +36,12 @@ enum AppEvent: Sendable, Equatable {
actor AppEventStream {
private var continuations: [UUID: AsyncStream<AppEvent>.Continuation] = [:]
func stream() -> AsyncStream<AppEvent> {
let id = UUID()
return AsyncStream { continuation in
continuations[id] = continuation
continuation.onTermination = { [id] _ in
Task {
await self.removeContinuation(id)
}
}
}
}
func emit(_ event: AppEvent) {
for continuation in continuations.values {
continuation.yield(event)
}
}
func finish() {
for continuation in continuations.values {
continuation.finish()
}
continuations.removeAll()
}
private func removeContinuation(_ id: UUID) {
continuations.removeValue(forKey: id)
}
}
/// Identity key for a direct conversation. Equality and hashing use the
-4
View File
@@ -18,8 +18,6 @@ final class AppRuntime: ObservableObject {
/// (docs/CONVERSATION-STORE-DESIGN.md). Owned here; the feature models
/// and `ChatViewModel` observe and mutate it through its intent API.
let conversations: ConversationStore
let peerIdentityStore: PeerIdentityStore
let locationPresenceStore: LocationPresenceStore
let publicChatModel: PublicChatModel
let privateInboxModel: PrivateInboxModel
let privateConversationModel: PrivateConversationModel
@@ -51,8 +49,6 @@ final class AppRuntime: ObservableObject {
let locationPresenceStore = LocationPresenceStore()
let locationManager = LocationChannelManager.shared
self.conversations = conversations
self.peerIdentityStore = peerIdentityStore
self.locationPresenceStore = locationPresenceStore
self.chatViewModel = ChatViewModel(
keychain: keychain,
idBridge: idBridge,
-10
View File
@@ -796,16 +796,6 @@ extension ConversationStore {
return messageIDs
}
/// Removes every direct conversation (panic clear).
func removeAllDirectConversations() {
let directIDs = conversationIDs.filter { id in
if case .direct = id { return true }
return false
}
for id in directIDs {
removeConversation(id)
}
}
}
// MARK: - Diagnostics support
-2
View File
@@ -1,4 +1,3 @@
import BitFoundation
import Combine
import Foundation
@@ -17,7 +16,6 @@ final class LocationChannelsModel: ObservableObject {
private let manager: LocationChannelManager
private let network: NetworkActivationService
private let gateway: GatewayService
private var cancellables = Set<AnyCancellable>()
init(
manager: LocationChannelManager? = nil,
-8
View File
@@ -25,10 +25,6 @@ final class PeerIdentityStore: ObservableObject {
stablePeerIDsByShortID[peerID] = stablePeerID
}
func replaceStablePeerIDs(_ mappings: [PeerID: PeerID]) {
stablePeerIDsByShortID = mappings
}
func fingerprint(for peerID: PeerID) -> String? {
peerFingerprintsByPeerID[peerID]
}
@@ -94,10 +90,6 @@ final class PeerIdentityStore: ObservableObject {
invalidateEncryptionCache(for: peerID)
}
func replaceEncryptionStatuses(_ statuses: [PeerID: EncryptionStatus]) {
encryptionStatuses = statuses
}
func setVerifiedFingerprints(_ fingerprints: Set<String>) {
verifiedFingerprints = fingerprints
}
-6
View File
@@ -3,7 +3,6 @@ import Combine
import Foundation
struct FingerprintPresentationState: Equatable {
let statusPeerID: PeerID
let peerNickname: String
let encryptionStatus: EncryptionStatus
let theirFingerprint: String?
@@ -56,10 +55,6 @@ final class VerificationModel: ObservableObject {
return VerificationService.shared.buildMyQRString(nickname: currentNickname, npub: npub) ?? ""
}
func beginQRVerification(with qr: VerificationService.VerificationQR) -> Bool {
chatViewModel.beginQRVerification(with: qr)
}
func verifyScannedPayload(_ payload: String) -> VerificationScanOutcome {
guard let qr = VerificationService.shared.verifyScannedQR(payload) else {
return .invalid
@@ -110,7 +105,6 @@ final class VerificationModel: ObservableObject {
}
return FingerprintPresentationState(
statusPeerID: statusPeerID,
peerNickname: peerNickname,
encryptionStatus: encryptionStatus,
theirFingerprint: theirFingerprint,
@@ -5,7 +5,6 @@ import AVFoundation
actor VoiceRecorder {
enum RecorderError: Error {
case microphoneAccessDenied
case recorderInitializationFailed
case recordingInProgress
}
-6
View File
@@ -56,12 +56,6 @@ final class WaveformCache {
}
}
func purgeAll() {
queue.async(flags: .barrier) { [weak self] in
self?.cache.removeAll()
}
}
private func computeWaveform(url: URL, bins: Int) -> [Float]? {
guard bins > 0 else { return nil }
// Use autoreleasepool to manage memory from audio buffer allocations
-7
View File
@@ -88,8 +88,6 @@ import BitFoundation
/// Represents the ephemeral layer of identity - short-lived peer IDs that provide network privacy.
/// These IDs rotate periodically to prevent tracking while maintaining cryptographic relationships.
struct EphemeralIdentity {
let peerID: PeerID // 8 random bytes
let sessionStart: Date
var handshakeState: HandshakeState
}
@@ -98,7 +96,6 @@ enum HandshakeState {
case initiated
case inProgress
case completed(fingerprint: String)
case failed(reason: String)
}
/// Represents the cryptographic layer of identity - the stable Noise Protocol static key pair.
@@ -110,7 +107,6 @@ struct CryptographicIdentity: Codable {
// Optional Ed25519 signing public key (used to authenticate public messages)
var signingPublicKey: Data? = nil
let firstSeen: Date
let lastHandshake: Date?
}
/// Represents the social layer of identity - user-assigned names and trust relationships.
@@ -193,9 +189,6 @@ struct IdentityCache: Codable {
// Fingerprint -> when we verified it (orders outgoing vouch batches;
// entries verified before this field exists sort as oldest)
var verifiedAt: [String: Date]? = nil
// Schema version for future migrations
var version: Int = 1
}
//
@@ -108,8 +108,6 @@ protocol SecureIdentityStateManagerProtocol {
func updateSocialIdentity(_ identity: SocialIdentity)
// MARK: Favorites Management
func getFavorites() -> Set<String>
func setFavorite(_ fingerprint: String, isFavorite: Bool)
func isFavorite(fingerprint: String) -> Bool
// MARK: Blocked Users Management
@@ -123,7 +121,6 @@ protocol SecureIdentityStateManagerProtocol {
// MARK: Ephemeral Session Management
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState)
func updateHandshakeState(peerID: PeerID, state: HandshakeState)
// MARK: Cleanup
func clearAllIdentityData()
@@ -139,7 +136,6 @@ protocol SecureIdentityStateManagerProtocol {
func recordVouch(voucheeFingerprint: String, voucherFingerprint: String, timestamp: Date) -> Bool
func validVouchers(for fingerprint: String) -> [VouchRecord]
func isVouched(fingerprint: String) -> Bool
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel
func lastVouchBatchSent(to fingerprint: String) -> Date?
func markVouchBatchSent(to fingerprint: String, at date: Date)
func signingPublicKey(forFingerprint fingerprint: String) -> Data?
@@ -322,21 +318,13 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
fingerprint: fingerprint,
publicKey: noisePublicKey,
signingPublicKey: signingPublicKey ?? existing.signingPublicKey,
firstSeen: existing.firstSeen,
lastHandshake: now
firstSeen: existing.firstSeen
)
self.cryptographicIdentities[fingerprint] = existing
} else {
// Update signing key and lastHandshake
// Update signing key
existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey
let updated = CryptographicIdentity(
fingerprint: existing.fingerprint,
publicKey: existing.publicKey,
signingPublicKey: existing.signingPublicKey,
firstSeen: existing.firstSeen,
lastHandshake: now
)
self.cryptographicIdentities[fingerprint] = updated
self.cryptographicIdentities[fingerprint] = existing
}
// Persist updated state (already assigned in branches above)
} else {
@@ -345,8 +333,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
fingerprint: fingerprint,
publicKey: noisePublicKey,
signingPublicKey: signingPublicKey,
firstSeen: now,
lastHandshake: now
firstSeen: now
)
self.cryptographicIdentities[fingerprint] = entry
}
@@ -511,11 +498,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState = .none) {
queue.async(flags: .barrier) {
self.ephemeralSessions[peerID] = EphemeralIdentity(
peerID: peerID,
sessionStart: Date(),
handshakeState: handshakeState
)
self.ephemeralSessions[peerID] = EphemeralIdentity(handshakeState: handshakeState)
}
}
File diff suppressed because it is too large Load Diff
@@ -13,13 +13,6 @@ extension BitchatMessage {
enum Media {
case voice(URL)
case image(URL)
var url: URL {
switch self {
case .voice(let url), .image(let url):
return url
}
}
}
// Cache the directory lookup to avoid repeated FileManager calls during view rendering
+1 -3
View File
@@ -7,7 +7,6 @@ struct BitchatPeer: Equatable {
let peerID: PeerID // Hex-encoded peer ID
let noisePublicKey: Data
let nickname: String
let lastSeen: Date
let isConnected: Bool
let isReachable: Bool
@@ -77,14 +76,13 @@ struct BitchatPeer: Equatable {
peerID: PeerID,
noisePublicKey: Data,
nickname: String,
lastSeen: Date = Date(),
lastSeen _: Date = Date(),
isConnected: Bool = false,
isReachable: Bool = false
) {
self.peerID = peerID
self.noisePublicKey = noisePublicKey
self.nickname = nickname
self.lastSeen = lastSeen
self.isConnected = isConnected
self.isReachable = isReachable
+1 -1
View File
@@ -723,7 +723,7 @@ final class NoiseHandshakeState {
return messageBuffer
}
func readMessage(_ message: Data, expectedPayloadLength: Int = 0) throws -> Data {
func readMessage(_ message: Data, expectedPayloadLength _: Int = 0) throws -> Data {
guard currentPattern < messagePatterns.count else {
throw NoiseError.handshakeComplete
@@ -21,12 +21,6 @@ enum NoiseSecurityConstants {
// Maximum number of messages before rekey (2^64 - 1 is the nonce limit)
static let maxMessagesPerSession: UInt64 = 1_000_000_000 // 1 billion messages
// Handshake timeout - abandon incomplete handshakes
static let handshakeTimeout: TimeInterval = 60 // 1 minute
// Maximum concurrent sessions per peer
static let maxSessionsPerPeer = 3
// Rate limiting
static let maxHandshakesPerMinute = 10
static let maxMessagesPerSecond = 100
-1
View File
@@ -14,5 +14,4 @@ enum NoiseSecurityError: Error {
case messageTooLarge
case invalidPeerID
case rateLimitExceeded
case handshakeTimeout
}
+2 -8
View File
@@ -13,8 +13,6 @@ import BitFoundation
final class NoiseSessionManager {
private var sessions: [PeerID: NoiseSession] = [:]
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
private let keychain: KeychainManagerProtocol
private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
@@ -23,8 +21,6 @@ final class NoiseSessionManager {
var onSessionFailed: ((PeerID, Error) -> Void)?
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
self.localStaticKey = localStaticKey
self.keychain = keychain
self.sessionFactory = { peerID, role in
SecureNoiseSession(
peerID: peerID,
@@ -37,12 +33,10 @@ final class NoiseSessionManager {
#if DEBUG
init(
localStaticKey: Curve25519.KeyAgreement.PrivateKey,
keychain: KeychainManagerProtocol,
localStaticKey _: Curve25519.KeyAgreement.PrivateKey,
keychain _: KeychainManagerProtocol,
sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession
) {
self.localStaticKey = localStaticKey
self.keychain = keychain
self.sessionFactory = sessionFactory
}
#endif
+1 -9
View File
@@ -6,14 +6,12 @@ struct NostrIdentity: Codable {
let privateKey: Data
let publicKey: Data
let npub: String // Bech32-encoded public key
let createdAt: Date
/// Memberwise initializer
init(privateKey: Data, publicKey: Data, npub: String, createdAt: Date) {
init(privateKey: Data, publicKey: Data, npub: String, createdAt _: Date) {
self.privateKey = privateKey
self.publicKey = publicKey
self.npub = npub
self.createdAt = createdAt
}
/// Generate a new Nostr identity
@@ -39,12 +37,6 @@ struct NostrIdentity: Codable {
self.privateKey = privateKeyData
self.publicKey = xOnlyPubkey
self.npub = try Bech32.encode(hrp: "npub", data: xOnlyPubkey)
self.createdAt = Date()
}
/// Get signing key for event signatures
func signingKey() throws -> P256K.Signing.PrivateKey {
try P256K.Signing.PrivateKey(dataRepresentation: privateKey)
}
/// Get Schnorr signing key for Nostr event signatures
-8
View File
@@ -37,14 +37,6 @@ final class NostrIdentityBridge {
return nostrIdentity
}
/// Associate a Nostr identity with a Noise public key (for favorites)
func associateNostrIdentity(_ nostrPubkey: String, with noisePublicKey: Data) {
let key = "nostr-noise-\(noisePublicKey.base64EncodedString())"
if let data = nostrPubkey.data(using: .utf8) {
keychain.save(key: key, data: data, service: keychainService, accessible: nil)
}
}
/// Get Nostr public key associated with a Noise public key
func getNostrPublicKey(for noisePublicKey: Data) -> String? {
let key = "nostr-noise-\(noisePublicKey.base64EncodedString())"
-34
View File
@@ -548,37 +548,6 @@ struct NostrProtocol {
return sharedSecretData
}
// Direct version that doesn't try to add prefixes
private static func deriveSharedSecretDirect(
privateKey: P256K.Schnorr.PrivateKey,
publicKey: Data
) throws -> Data {
// Direct shared secret calculation
// Convert Schnorr private key to KeyAgreement private key
let keyAgreementPrivateKey = try P256K.KeyAgreement.PrivateKey(
dataRepresentation: privateKey.dataRepresentation
)
// Use the public key as-is (should already have prefix)
let keyAgreementPublicKey = try P256K.KeyAgreement.PublicKey(
dataRepresentation: publicKey,
format: .compressed
)
// Perform ECDH
let sharedSecret = try keyAgreementPrivateKey.sharedSecretFromKeyAgreement(
with: keyAgreementPublicKey,
format: .compressed
)
// Convert SharedSecret to Data
let sharedSecretData = sharedSecret.withUnsafeBytes { Data($0) }
// Return raw ECDH shared secret; HKDF is applied by deriveNIP44V2Key
return sharedSecretData
}
private static func randomizedTimestamp() -> Date {
// Add random offset to current time for privacy
// This prevents timing correlation attacks while the actual message timestamp
@@ -708,11 +677,8 @@ struct NostrEvent: Codable {
enum NostrError: Error {
case invalidPublicKey
case invalidPrivateKey
case invalidEvent
case invalidCiphertext
case signingFailed
case encryptionFailed
}
// MARK: - NIP-44 v2 helpers (XChaCha20-Poly1305)
-3
View File
@@ -117,7 +117,6 @@ final class NostrRelayManager: ObservableObject {
let url: String
var isConnected: Bool = false
var lastError: Error?
var lastConnectedAt: Date?
var messagesSent: Int = 0
var messagesReceived: Int = 0
var reconnectAttempts: Int = 0
@@ -347,7 +346,6 @@ final class NostrRelayManager: ObservableObject {
relays[index].nextReconnectTime = nil
if resetState {
relays[index].lastError = nil
relays[index].lastConnectedAt = nil
relays[index].lastDisconnectedAt = nil
relays[index].messagesSent = 0
relays[index].messagesReceived = 0
@@ -1075,7 +1073,6 @@ final class NostrRelayManager: ObservableObject {
relays[index].isConnected = isConnected
relays[index].lastError = error
if isConnected {
relays[index].lastConnectedAt = dependencies.now()
relays[index].reconnectAttempts = 0 // Reset on successful connection
relays[index].nextReconnectTime = nil
} else {
-8
View File
@@ -10,14 +10,6 @@ enum Geohash {
return map
}()
/// Validates a geohash string for building-level precision (8 characters).
/// - Parameter geohash: The geohash string to validate
/// - Returns: true if valid 8-character base32 geohash, false otherwise
static func isValidBuildingGeohash(_ geohash: String) -> Bool {
guard geohash.count == 8 else { return false }
return geohash.lowercased().allSatisfy { base32Map[$0] != nil }
}
/// Validates a geohash string at any channel precision (1-12 characters).
/// - Parameter geohash: The geohash string to validate
/// - Returns: true if a non-empty base32 geohash of at most 12 characters
@@ -11,13 +11,6 @@ import Foundation
/// Manages autocomplete functionality for chat
final class AutocompleteService {
private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: [])
private let commandRegex = try? NSRegularExpression(pattern: "^/([a-z]*)$", options: [])
private let commands = [
"/msg", "/who", "/clear",
"/hug", "/slap", "/fav", "/unfav",
"/block", "/unblock"
]
/// Get autocomplete suggestions for current text
func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) {
@@ -73,26 +66,6 @@ final class AutocompleteService {
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
}
private func getCommandSuggestions(_ text: String) -> ([String], NSRange)? {
guard let regex = commandRegex else { return nil }
let nsText = text as NSString
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
guard let match = matches.last else { return nil }
let fullRange = match.range(at: 0)
let captureRange = match.range(at: 1)
let prefix = nsText.substring(with: captureRange).lowercased()
let suggestions = commands
.filter { $0.hasPrefix("/\(prefix)") }
.sorted()
.prefix(5)
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
}
private func needsArgument(command: String) -> Bool {
switch command {
case "/who", "/clear":
@@ -61,7 +61,6 @@ struct BLEFragmentAssemblyBuffer {
}
private struct Metadata {
let type: UInt8
let total: Int
let timestamp: Date
let isBroadcast: Bool
@@ -150,7 +149,6 @@ struct BLEFragmentAssemblyBuffer {
fragmentsByKey[header.key] = [:]
metadataByKey[header.key] = Metadata(
type: header.originalType,
total: header.total,
timestamp: now,
isBroadcast: header.isBroadcastFragment,
@@ -25,9 +25,4 @@ final class BLELogRateLimiter {
}
}
func removeAll() {
queue.sync {
lastLogTimeByKey.removeAll()
}
}
}
@@ -20,7 +20,7 @@ enum BLEOutboundPacketPolicy {
}
}
static func priority(for packet: BitchatPacket, data: Data) -> BLEOutboundWritePriority {
static func priority(for packet: BitchatPacket, data _: Data) -> BLEOutboundWritePriority {
guard let messageType = MessageType(rawValue: packet.type) else { return .low }
switch messageType {
case .fragment:
@@ -141,14 +141,6 @@ struct BLEPeerRegistry {
}
}
func collisionResolvedNickname(for peerID: PeerID, selfNickname: String) -> String? {
guard let info = peers[peerID], info.isVerifiedNickname else { return nil }
let hasCollision = peers.values.contains {
$0.isConnected && $0.nickname == info.nickname && $0.peerID != peerID
} || selfNickname == info.nickname
return hasCollision ? info.nickname + "#" + String(peerID.id.prefix(4)) : info.nickname
}
mutating func markDisconnected(_ peerID: PeerID) {
guard var info = peers[peerID] else { return }
info.isConnected = false
+4 -14
View File
@@ -10,7 +10,6 @@ import UIKit
/// BLEService Bluetooth Mesh Transport
/// - Emits events exclusively via `BitchatDelegate` for UI.
/// - ChatViewModel must consume delegate callbacks (`didReceivePublicMessage`, `didReceiveNoisePayload`).
/// - A lightweight `peerSnapshotPublisher` is provided for non-UI services.
final class BLEService: NSObject {
// MARK: - Constants
@@ -494,13 +493,6 @@ final class BLEService: NSObject {
weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate?
// MARK: Peer snapshots publisher (non-UI convenience)
private let peerSnapshotSubject = PassthroughSubject<[TransportPeerSnapshot], Never>()
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
peerSnapshotSubject.eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] {
collectionsQueue.sync {
peerRegistry.transportSnapshots(selfNickname: myNickname)
@@ -1323,7 +1315,7 @@ final class BLEService: NSObject {
}
}
private func handleLeave(_ packet: BitchatPacket, from peerID: PeerID) {
private func handleLeave(_: BitchatPacket, from peerID: PeerID) {
_ = collectionsQueue.sync(flags: .barrier) {
// Remove the peer when they leave
peerRegistry.remove(peerID)
@@ -2174,7 +2166,7 @@ extension BLEService: CBPeripheralDelegate {
}
}
private func processNotificationPacket(_ packet: BitchatPacket, from peripheral: CBPeripheral, peripheralUUID: String, receivedFrom peerID: PeerID) {
private func processNotificationPacket(_ packet: BitchatPacket, from _: CBPeripheral, peripheralUUID: String, receivedFrom peerID: PeerID) {
let senderID = PeerID(hexData: packet.senderID)
if packet.type != MessageType.announce.rawValue {
@@ -3422,7 +3414,7 @@ extension BLEService {
/// Transport-level handling for a received nostrCarrier packet; policy
/// (verification of the carried event, quotas, loop prevention) lives in
/// `GatewayService` behind `onNostrCarrierPacket`.
private func handleNostrCarrier(_ packet: BitchatPacket, from peerID: PeerID) {
private func handleNostrCarrier(_ packet: BitchatPacket, from _: PeerID) {
let senderID = PeerID(hexData: packet.senderID)
let directedToUs: Bool
if let recipientID = packet.recipientID {
@@ -4476,7 +4468,7 @@ extension BLEService {
/// gossip backfill and hand the payload to the UI layer, where the group
/// coordinator decrypts and authenticates against the roster. Non-members
/// still relay (generic broadcast relay path) but never decode.
private func handleGroupMessage(_ packet: BitchatPacket, from peerID: PeerID) {
private func handleGroupMessage(_ packet: BitchatPacket, from _: PeerID) {
let isBroadcastRecipient: Bool = {
guard let recipient = packet.recipientID else { return true }
return recipient.count == 8 && recipient.allSatisfy { $0 == 0xFF }
@@ -4636,8 +4628,6 @@ extension BLEService {
let transportPeers: [TransportPeerSnapshot] = collectionsQueue.sync {
peerRegistry.transportSnapshots(selfNickname: myNickname)
}
// Notify non-UI listeners
peerSnapshotSubject.send(transportPeers)
// Notify UI on MainActor via delegate
Task { @MainActor [weak self] in
self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers)
@@ -19,7 +19,6 @@ final class BoardManager: ObservableObject {
@Published private(set) var posts: [BoardPostPacket] = []
private let transport: Transport
private let store: BoardStore
/// Publishes a bridged kind-1 note (expiring with the board post via
/// NIP-40) and returns its Nostr event id, or nil when bridging failed or
/// was skipped.
@@ -39,7 +38,6 @@ final class BoardManager: ObservableObject {
deleteFromNostr: ((String, String) -> Void)? = nil
) {
self.transport = transport
self.store = store
self.publishToNostr = publishToNostr ?? Self.livePublishToNostr
self.deleteFromNostr = deleteFromNostr ?? Self.liveDeleteFromNostr
cancellable = store.$postsSnapshot
-8
View File
@@ -146,14 +146,6 @@ final class BoardStore {
// MARK: - Maintenance
func pruneExpired() {
let nowMs = currentMs()
queue.sync {
pruneExpiredLocked(nowMs: nowMs)
persistLocked()
}
}
/// Panic wipe: drop all board data from memory and disk.
func wipe() {
queue.sync {
@@ -272,14 +272,6 @@ final class CourierStore {
// MARK: - Maintenance
func pruneExpired() {
let date = now()
queue.sync {
pruneExpiredLocked(at: date)
persistLocked()
}
}
/// Panic wipe: drop all carried mail from memory and disk.
func wipe() {
queue.sync {
@@ -58,12 +58,6 @@ final class StoreAndForwardMetrics {
SecureLogger.debug("📊 S&F \(event.rawValue)\(total)", category: .session)
}
func snapshot() -> [String: Int] {
lock.lock()
defer { lock.unlock() }
return counts
}
/// Included in the panic wipe alongside the stores it describes.
func reset() {
lock.lock()
@@ -82,7 +82,6 @@ final class GatewayService: ObservableObject {
let depositor: PeerID
let geohash: String
let event: NostrEvent
let queuedAt: Date
}
static let shared = GatewayService()
@@ -221,7 +220,7 @@ final class GatewayService: ObservableObject {
publish(event, geohash: carrier.geohash)
accepted = true
} else {
accepted = enqueueUplink(QueuedUplink(depositor: depositor, geohash: carrier.geohash, event: event, queuedAt: now()))
accepted = enqueueUplink(QueuedUplink(depositor: depositor, geohash: carrier.geohash, event: event))
}
// Only render on our own timeline what we actually accepted for
@@ -9,12 +9,12 @@
import Foundation
/// Represents a participant in a geohash channel
public struct GeoPerson: Identifiable, Equatable, Sendable {
public let id: String // pubkey hex (lowercased)
public let displayName: String
public let lastSeen: Date
struct GeoPerson: Identifiable, Equatable, Sendable {
let id: String // pubkey hex (lowercased)
let displayName: String
let lastSeen: Date
public init(id: String, displayName: String, lastSeen: Date) {
init(id: String, displayName: String, lastSeen: Date) {
self.id = id
self.displayName = displayName
self.lastSeen = lastSeen
@@ -23,7 +23,7 @@ public struct GeoPerson: Identifiable, Equatable, Sendable {
/// Protocol for resolving display names and checking block status
@MainActor
public protocol GeohashParticipantContext: AnyObject {
protocol GeohashParticipantContext: AnyObject {
/// Returns display name for a Nostr pubkey (e.g., "alice#a1b2" or "anon#c3d4")
func displayNameForPubkey(_ pubkeyHex: String) -> String
/// Returns true if the pubkey is blocked
@@ -32,16 +32,16 @@ public protocol GeohashParticipantContext: AnyObject {
/// Tracks participants across multiple geohash channels
@MainActor
public final class GeohashParticipantTracker: ObservableObject {
final class GeohashParticipantTracker: ObservableObject {
/// Activity cutoff duration (defaults to 5 minutes)
public let activityCutoff: TimeInterval
let activityCutoff: TimeInterval
/// Per-geohash participant map: [geohash: [pubkeyHex: lastSeen]]
private var participants: [String: [String: Date]] = [:]
/// Currently visible people for the active geohash
@Published public private(set) var visiblePeople: [GeoPerson] = []
@Published private(set) var visiblePeople: [GeoPerson] = []
/// The currently active geohash (if any)
private var activeGeohash: String?
@@ -52,17 +52,17 @@ public final class GeohashParticipantTracker: ObservableObject {
/// Timer for periodic refresh
private var refreshTimer: Timer?
public init(activityCutoff: TimeInterval = -300) { // default 5 minutes
init(activityCutoff: TimeInterval = -300) { // default 5 minutes
self.activityCutoff = activityCutoff
}
/// Configure with a context provider
public func configure(context: GeohashParticipantContext) {
func configure(context: GeohashParticipantContext) {
self.context = context
}
/// Set the currently active geohash
public func setActiveGeohash(_ geohash: String?) {
func setActiveGeohash(_ geohash: String?) {
activeGeohash = geohash
if geohash == nil {
visiblePeople = []
@@ -72,13 +72,13 @@ public final class GeohashParticipantTracker: ObservableObject {
}
/// Record activity from a participant in the current active geohash
public func recordParticipant(pubkeyHex: String) {
func recordParticipant(pubkeyHex: String) {
guard let gh = activeGeohash else { return }
recordParticipant(pubkeyHex: pubkeyHex, geohash: gh)
}
/// Record activity from a participant in a specific geohash
public func recordParticipant(pubkeyHex: String, geohash: String) {
func recordParticipant(pubkeyHex: String, geohash: String) {
let key = pubkeyHex.lowercased()
var map = participants[geohash] ?? [:]
map[key] = Date()
@@ -94,7 +94,7 @@ public final class GeohashParticipantTracker: ObservableObject {
}
/// Remove a participant from all geohashes (used when blocking)
public func removeParticipant(pubkeyHex: String) {
func removeParticipant(pubkeyHex: String) {
let key = pubkeyHex.lowercased()
for (gh, var map) in participants {
map.removeValue(forKey: key)
@@ -104,14 +104,14 @@ public final class GeohashParticipantTracker: ObservableObject {
}
/// Get participant count for a specific geohash
public func participantCount(for geohash: String) -> Int {
func participantCount(for geohash: String) -> Int {
let cutoff = Date().addingTimeInterval(activityCutoff)
let map = participants[geohash] ?? [:]
return map.values.filter { $0 >= cutoff }.count
}
/// Get the visible people list for the active geohash (read-only query)
public func getVisiblePeople() -> [GeoPerson] {
func getVisiblePeople() -> [GeoPerson] {
guard let gh = activeGeohash, let context = context else { return [] }
let cutoff = Date().addingTimeInterval(activityCutoff)
let map = (participants[gh] ?? [:])
@@ -126,12 +126,12 @@ public final class GeohashParticipantTracker: ObservableObject {
}
/// Refresh the visible people list
public func refresh() {
func refresh() {
visiblePeople = getVisiblePeople()
}
/// Start the periodic refresh timer
public func startRefreshTimer(interval: TimeInterval = 30.0) {
func startRefreshTimer(interval: TimeInterval = 30.0) {
stopRefreshTimer()
refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
Task { @MainActor in
@@ -141,19 +141,19 @@ public final class GeohashParticipantTracker: ObservableObject {
}
/// Stop the periodic refresh timer
public func stopRefreshTimer() {
func stopRefreshTimer() {
refreshTimer?.invalidate()
refreshTimer = nil
}
/// Clear all participant data
public func clear() {
func clear() {
participants.removeAll()
visiblePeople = []
}
/// Clear participant data for a specific geohash
public func clear(geohash: String) {
func clear(geohash: String) {
participants.removeValue(forKey: geohash)
if activeGeohash == geohash {
visiblePeople = []
@@ -406,7 +406,6 @@ enum GroupCryptoError: Error, Equatable {
case malformedPayload
case signingFailed
case sealFailed
case wrongEpoch
case decryptionFailed
case badSenderSignature
}
-25
View File
@@ -255,11 +255,6 @@ final class KeychainManager: KeychainManagerProtocol {
// MARK: - Generic Operations
private func save(_ value: String, forKey key: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
return saveData(data, forKey: key)
}
private func saveData(_ data: Data, forKey key: String) -> Bool {
// Delete any existing item first to ensure clean state
_ = delete(forKey: key)
@@ -305,11 +300,6 @@ final class KeychainManager: KeychainManagerProtocol {
return false
}
private func retrieve(forKey key: String) -> String? {
guard let data = retrieveData(forKey: key) else { return nil }
return String(data: data, encoding: .utf8)
}
private func retrieveData(forKey key: String) -> Data? {
// Base query
let base: [String: Any] = [
@@ -366,21 +356,6 @@ final class KeychainManager: KeychainManagerProtocol {
// MARK: - Cleanup
func deleteAllPasswords() -> Bool {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword
]
// Add service if not empty
if !service.isEmpty {
query[kSecAttrService as String] = service
}
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
// Delete ALL keychain data for panic mode
func deleteAllKeychainData() -> Bool {
SecureLogger.warning("Panic mode - deleting all keychain data", category: .security)
+1 -16
View File
@@ -101,9 +101,7 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
private let cl: LocationStateManaging
private let geocoder: LocationStateGeocoding
private var lastLocation: CLLocation?
private var refreshTimer: Timer?
private var isGeocoding: Bool = false
// MARK: - Persistence Keys
@@ -268,7 +266,7 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
}
}
func beginLiveRefresh(interval: TimeInterval = TransportConfig.locationLiveRefreshInterval) {
func beginLiveRefresh(interval _: TimeInterval = TransportConfig.locationLiveRefreshInterval) {
guard permissionState == .authorized else { return }
refreshTimer?.invalidate()
refreshTimer = nil
@@ -385,7 +383,6 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let loc = locations.last else { return }
lastLocation = loc
computeChannels(from: loc.coordinate)
reverseGeocodeLocation(loc)
}
@@ -441,10 +438,8 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
private func reverseGeocodeLocation(_ location: CLLocation) {
geocoder.cancelGeocode()
isGeocoding = true
geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, _ in
guard let self = self else { return }
self.isGeocoding = false
if let pm = placemarks?.first {
let names = self.locationNamesByLevel(from: pm)
Task { @MainActor in self.locationNames = names }
@@ -632,15 +627,5 @@ extension LocationStateManager {
func toggle(_ geohash: String) {
toggleBookmark(geohash)
}
/// Backward compatibility: add bookmark (was GeohashBookmarksStore.add)
func add(_ geohash: String) {
addBookmark(geohash)
}
/// Backward compatibility: remove bookmark (was GeohashBookmarksStore.remove)
func remove(_ geohash: String) {
removeBookmark(geohash)
}
}
#endif
@@ -70,10 +70,6 @@ final class MessageFormattingEngine {
static let quickCashuPresence: NSRegularExpression = {
try! NSRegularExpression(pattern: "\\bcashu[AB][A-Za-z0-9._-]{40,}\\b", options: [])
}()
static let simplifyHTTPURL: NSRegularExpression = {
try! NSRegularExpression(pattern: "https?://[^\\s?#]+(?:[?#][^\\s]*)?", options: [.caseInsensitive])
}()
}
// MARK: - Match Types
@@ -195,7 +191,7 @@ final class MessageFormattingEngine {
// MARK: - Private Helpers
private static func formatSystemMessage(_ message: BitchatMessage, isDark: Bool) -> AttributedString {
private static func formatSystemMessage(_ message: BitchatMessage, isDark _: Bool) -> AttributedString {
var result = AttributedString()
let content = AttributedString("* \(message.content) *")
@@ -414,7 +410,7 @@ final class MessageFormattingEngine {
return AttributedString(text).mergingAttributes(style)
}
private static func formatMatch(_ text: String, type: MatchType, baseColor: Color, isSelf: Bool) -> AttributedString {
private static func formatMatch(_ text: String, type: MatchType, baseColor _: Color, isSelf _: Bool) -> AttributedString {
var style = AttributeContainer()
switch type {
-7
View File
@@ -311,13 +311,6 @@ final class MessageRouter {
}
}
func sendDeliveryAck(_ messageID: String, to peerID: PeerID) {
if let transport = reachableTransport(for: peerID) {
SecureLogger.debug("Routing DELIVERED ack via \(type(of: transport)) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))", category: .session)
transport.sendDeliveryAck(for: messageID, to: peerID)
}
}
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
if let transport = connectedTransport(for: peerID) {
transport.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
@@ -153,11 +153,11 @@ enum EncryptionStatus: Equatable {
final class NoiseEncryptionService {
// Static identity key (persistent across sessions)
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
// Ed25519 signing key (persistent across sessions)
private let signingKey: Curve25519.Signing.PrivateKey
public let signingPublicKey: Curve25519.Signing.PublicKey
let signingPublicKey: Curve25519.Signing.PublicKey
// Session manager
private let sessionManager: NoiseSessionManager
+1 -10
View File
@@ -13,7 +13,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
let currentIdentity: @MainActor () throws -> NostrIdentity?
let registerPendingGiftWrap: @MainActor (String) -> Void
let sendEvent: @MainActor (NostrEvent) -> Void
let scheduleAfter: @Sendable (TimeInterval, @escaping @Sendable () -> Void) -> Void
/// Emits whether a relay that carries private messages is up
/// (fail-closed behind Tor). A connected geohash/custom relay alone
/// doesn't count: DM sends target the default relay set and would
@@ -42,7 +41,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
self.currentIdentity = currentIdentity
self.registerPendingGiftWrap = registerPendingGiftWrap
self.sendEvent = sendEvent
self.scheduleAfter = scheduleAfter
self.relayConnectivity = relayConnectivity
// Default pacer drives its throttle through the same injected
// scheduler, so tests that step scheduleAfter manually keep
@@ -130,8 +128,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
}
}
static let sharedAckPacer = AckPacer()
private let keychain: KeychainManagerProtocol
private let idBridge: NostrIdentityBridge
private let dependencies: Dependencies
private var favoriteStatusObserver: NSObjectProtocol?
@@ -145,12 +141,10 @@ final class NostrTransport: Transport, @unchecked Sendable {
@MainActor
init(
keychain: KeychainManagerProtocol,
keychain _: KeychainManagerProtocol,
idBridge: NostrIdentityBridge,
dependencies: Dependencies? = nil
) {
self.keychain = keychain
self.idBridge = idBridge
self.dependencies = dependencies ?? .live(idBridge: idBridge)
setupObservers()
@@ -207,9 +201,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate?
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
Just([]).eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] }
var myPeerID: PeerID { senderPeerID }
+1 -14
View File
@@ -28,7 +28,6 @@ final class PrivateChatManager: ObservableObject {
@Published private(set) var selectedPeer: PeerID? = nil
private var selectedPeerMirrorCancellable: AnyCancellable? = nil
private var selectedPeerFingerprint: String? = nil
var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel
weak var meshService: Transport?
@@ -219,18 +218,13 @@ final class PrivateChatManager: ObservableObject {
/// Start a private chat with a peer. Selection is mutated through the
/// store's intent (the store owns it); the manager keeps its side
/// effects (fingerprint tracking, read receipts, unread clearing).
/// effects (read receipts, unread clearing).
@MainActor
func startChat(with peerID: PeerID) {
// Also creates the conversation if needed and updates the derived
// `selectedConversationID`; `selectedPeer` mirrors the change.
conversationStore?.setSelectedPrivatePeer(peerID)
// Store fingerprint for persistence across reconnections
if let fingerprint = meshService?.getFingerprint(for: peerID) {
selectedPeerFingerprint = fingerprint
}
// Mark messages as read
markAsRead(from: peerID)
}
@@ -239,15 +233,8 @@ final class PrivateChatManager: ObservableObject {
/// channel's conversation).
func endChat() {
conversationStore?.setSelectedPrivatePeer(nil)
selectedPeerFingerprint = nil
}
/// No-op since the `ConversationStore` cutover: the store maintains
/// chronological order and dedups by message ID on every insert, so the
/// per-append re-sort/dedup sweep this performed is no longer needed.
/// Kept only for API compatibility until step 5 removes the callers.
func sanitizeChat(for peerID: PeerID) {}
/// Mark messages from a peer as read
@MainActor
func markAsRead(from peerID: PeerID) {
+1 -1
View File
@@ -12,7 +12,7 @@ struct RelayController {
static func decide(ttl: UInt8,
senderIsSelf: Bool,
recipientIsSelf: Bool = false,
isEncrypted: Bool,
isEncrypted _: Bool,
isDirectedEncrypted: Bool,
isFragment: Bool,
isDirectedFragment: Bool,
@@ -49,12 +49,6 @@ final class TransferProgressManager {
}
}
func reset(id: String) {
queue.async(flags: .barrier) { [weak self] in
self?.states.removeValue(forKey: id)
}
}
func snapshot(id: String) -> (sent: Int, total: Int)? {
var result: (sent: Int, total: Int)?
queue.sync {
+1 -2
View File
@@ -96,7 +96,6 @@ protocol Transport: AnyObject {
var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
// Peer snapshots (for non-UI services)
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
func currentPeerSnapshots() -> [TransportPeerSnapshot]
// Identity
@@ -265,7 +264,7 @@ extension Transport {
}
protocol TransportPeerEventsDelegate: AnyObject {
@MainActor func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot])
@MainActor func didUpdatePeerSnapshots(_: [TransportPeerSnapshot])
}
extension BitchatDelegate {
-15
View File
@@ -27,7 +27,6 @@ enum TransportConfig {
static let pttJitterBufferSeconds: TimeInterval = 0.35 // buffered audio before live playback starts
static let pttJitterDeadlineSeconds: TimeInterval = 0.5 // start anyway after this wall-clock wait
static let pttBurstEndTimeoutSeconds: TimeInterval = 3.0 // no frames -> burst considered ended
static let pttAssemblyStaleSeconds: TimeInterval = 30.0 // drop half-open assemblies after this
static let pttMaxConcurrentAssemblies: Int = 8 // concurrent inbound bursts cap
static let pttMaxBurstBytes: Int = 384 * 1024 // 120s at ~2KB/s + generous slack
static let pttFinishedBurstRegistrySeconds: TimeInterval = 600 // window to absorb the finalized note
@@ -93,7 +92,6 @@ enum TransportConfig {
// UI thresholds
static let uiProcessedNostrEventsCap: Int = 2000
static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60
// UI rate limiters (token buckets)
static let uiSenderRateBucketCapacity: Double = 5
@@ -103,17 +101,13 @@ enum TransportConfig {
// UI sleeps/delays
static let uiStartupInitialDelaySeconds: TimeInterval = 1.0
static let uiStartupShortSleepNs: UInt64 = 200_000_000
static let uiStartupPhaseDurationSeconds: TimeInterval = 2.0
static let uiAsyncShortSleepNs: UInt64 = 100_000_000
static let uiAsyncMediumSleepNs: UInt64 = 500_000_000
static let uiReadReceiptRetryShortSeconds: TimeInterval = 0.1
static let uiReadReceiptRetryLongSeconds: TimeInterval = 0.5
static let uiBatchDispatchStaggerSeconds: TimeInterval = 0.15
static let uiScrollThrottleSeconds: TimeInterval = 0.5
static let uiAnimationShortSeconds: TimeInterval = 0.15
static let uiAnimationMediumSeconds: TimeInterval = 0.2
static let uiAnimationSidebarSeconds: TimeInterval = 0.25
static let uiRecentCutoffFiveMinutesSeconds: TimeInterval = 5 * 60
static let uiMeshEmptyConfirmationSeconds: TimeInterval = 30.0
@@ -179,10 +173,6 @@ enum TransportConfig {
static let nostrGeohashSampleLimit: Int = 100
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
// Nostr helpers
static let nostrShortKeyDisplayLength: Int = 8
static let nostrConvKeyPrefixLength: Int = 16
// Message deduplication
static let messageDedupMaxAgeSeconds: TimeInterval = 300
static let messageDedupMaxCount: Int = 1000
@@ -295,11 +285,6 @@ enum TransportConfig {
static let uiLongMessageLineLimit: Int = 30
static let uiFingerprintSampleCount: Int = 3
// UI swipe/gesture thresholds
static let uiBackSwipeTranslationLarge: CGFloat = 50
static let uiBackSwipeTranslationSmall: CGFloat = 30
static let uiBackSwipeVelocityThreshold: CGFloat = 300
// UI color tuning
static let uiColorHueAvoidanceDelta: Double = 0.05
static let uiColorHueOffset: Double = 0.12
+1 -6
View File
@@ -70,7 +70,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
}
// TransportPeerEventsDelegate
func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot]) {
func didUpdatePeerSnapshots(_: [TransportPeerSnapshot]) {
updatePeers()
}
@@ -368,11 +368,6 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
// MARK: - Compatibility Methods (for easy migration)
var allPeers: [BitchatPeer] { peers }
var connectedPeers: Set<PeerID> { connectedPeerIDs }
var favoritePeers: Set<String> {
Set(favorites.compactMap { getFingerprint(for: $0.peerID) })
}
var blockedUsers: Set<String> {
Set(peers.compactMap { peer in
isBlocked(peer.peerID) ? getFingerprint(for: peer.peerID) : nil
-19
View File
@@ -52,18 +52,6 @@ final class MessageDeduplicator {
return false
}
/// Record an ID with a specific timestamp (for content key tracking)
func record(_ id: String, timestamp: Date) {
lock.lock()
defer { lock.unlock() }
if lookup[id] == nil {
entries.append(Entry(id: id, timestamp: timestamp))
}
lookup[id] = timestamp
trimIfNeeded()
}
/// Add an ID without checking (for announce-back tracking)
func markProcessed(_ id: String) {
lock.lock()
@@ -83,13 +71,6 @@ final class MessageDeduplicator {
return lookup[id] != nil
}
/// Get timestamp for an ID (for content deduplication time-window checks)
func timestampFor(_ id: String) -> Date? {
lock.lock()
defer { lock.unlock() }
return lookup[id]
}
private func trimIfNeeded() {
let activeCount = entries.count - head
guard activeCount > maxCount else { return }
+1 -1
View File
@@ -96,7 +96,7 @@ struct ThemePalette {
)
}
static func liquidGlass(_ colorScheme: ColorScheme) -> ThemePalette {
static func liquidGlass(_: ColorScheme) -> ThemePalette {
ThemePalette(
background: systemBackground,
primary: .primary,
@@ -387,7 +387,7 @@ final class ChatGroupCoordinator {
/// Decrypt-verify path for an incoming 0x25 broadcast. Drops silently for
/// unknown groups (non-members relay but never read), wrong epochs, bad
/// sender signatures, and senders missing from the pinned roster.
func handleGroupMessagePayload(_ payload: Data, timestamp: Date) {
func handleGroupMessagePayload(_ payload: Data, timestamp _: Date) {
guard let envelope = GroupMessageEnvelope.decode(payload) else { return }
guard let group = context.groupStore.group(withID: envelope.groupID) else { return }
guard envelope.epoch == group.epoch else {
@@ -29,7 +29,6 @@ protocol ChatPeerIdentityContext: AnyObject {
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
var selectedPrivateChatPeer: PeerID? { get set }
var selectedPrivateChatFingerprint: String? { get set }
var nickname: String { get }
var myPeerID: PeerID { get }
var activeChannel: ChannelID { get }
/// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`).
@@ -104,7 +103,7 @@ protocol ChatPeerIdentityContext: AnyObject {
extension ChatViewModel: ChatPeerIdentityContext {
// `privateChats`, `unreadPrivateMessages`, `selectedPrivateChatPeer`,
// `selectedPrivateChatFingerprint`, `nickname`, `myPeerID`,
// `selectedPrivateChatFingerprint`, `myPeerID`,
// `activeChannel`, `connectedPeers`, `geoNicknames`, `notifyUIChanged()`,
// `addSystemMessage(_:)`, `peerNickname(for:)`, `meshPeerNicknames()`,
// `ephemeralPeerID(forNoiseKey:)`, `unifiedPeer(for:)`,
@@ -19,7 +19,6 @@ protocol ChatPrivateConversationContext: AnyObject {
/// lookup on `ChatViewModel` (no `privateChats` dictionary build).
func privateMessages(for peerID: PeerID) -> [BitchatMessage]
var sentReadReceipts: Set<String> { get }
var unreadPrivateMessages: Set<PeerID> { get }
var selectedPrivateChatPeer: PeerID? { get }
var nickname: String { get }
var activeChannel: ChannelID { get }
@@ -40,8 +39,6 @@ protocol ChatPrivateConversationContext: AnyObject {
func setPrivateDeliveryStatus(_ status: DeliveryStatus, forMessageID messageID: String, peerID: PeerID) -> Bool
func markPrivateChatUnread(_ peerID: PeerID)
func markPrivateChatRead(_ peerID: PeerID)
/// Removes the peer's chat entirely, including unread state.
func removePrivateChat(_ peerID: PeerID)
/// Moves all messages from `oldPeerID`'s chat into `newPeerID`'s chat
/// (dedup by ID, order preserved, unread carried, old chat removed).
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
@@ -87,7 +84,6 @@ protocol ChatPrivateConversationContext: AnyObject {
// MARK: Routing & acknowledgements
func routePrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String)
func routeReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
func sendGeohashPrivateMessage(_ content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String)
func sendGeohashDeliveryAck(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity)
@@ -606,7 +602,7 @@ final class ChatPrivateConversationCoordinator {
/// O(1)-per-conversation dedup via the store's message-ID indexes
/// (replaces the full scan over every private chat).
func isDuplicateMessage(_ messageId: String, targetPeerID: PeerID) -> Bool {
func isDuplicateMessage(_ messageId: String, targetPeerID _: PeerID) -> Bool {
context.privateChatsContainMessage(withID: messageId)
}
@@ -825,25 +821,6 @@ final class ChatPrivateConversationCoordinator {
}
}
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
var noiseKey: Data?
if let hexKey = Data(hexString: peerID.id) {
noiseKey = hexKey
} else if let peerNoiseKey = context.noisePublicKey(for: peerID) {
noiseKey = peerNoiseKey
}
if context.isPeerConnected(peerID) {
context.routeFavoriteNotification(to: peerID, isFavorite: isFavorite)
SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session)
} else if let key = noiseKey {
context.routeFavoriteNotification(to: PeerID(hexData: key), isFavorite: isFavorite)
} else {
SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session)
}
}
func isMessageBlocked(_ message: BitchatMessage) -> Bool {
if let peerID = message.senderPeerID ?? context.getPeerIDForNickname(message.sender) {
if context.isPeerBlocked(peerID) { return true }
@@ -36,9 +36,6 @@ protocol ChatPublicConversationContext: AnyObject {
/// message with the same ID is already in that conversation.
@discardableResult
func appendPublicMessage(_ message: BitchatMessage, to conversationID: ConversationID) -> Bool
/// Appends a geohash message if absent. Returns `true` when stored.
@discardableResult
func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool
func publicConversationContainsMessage(withID messageID: String, in conversationID: ConversationID) -> Bool
/// Removes a message by ID from whichever public conversation contains it.
@discardableResult
@@ -109,7 +106,6 @@ extension ChatViewModel: ChatPublicConversationContext {
// `geoParticipantCount(for:)`, `isNostrBlocked(pubkeyHexLowercased:)`,
// `deriveNostrIdentity(forGeohash:)`, the public conversation store
// intents (`appendPublicMessage(_:to:)`,
// `appendGeohashMessageIfAbsent(_:toGeohash:)`,
// `publicConversationContainsMessage(withID:in:)`,
// `removePublicMessage(withID:)`,
// `removePublicMessages(fromGeohash:where:)`,
@@ -520,27 +516,27 @@ final class ChatPublicConversationCoordinator: PublicMessagePipelineDelegate {
#endif
}
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String {
func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String {
context.normalizedContentKey(content)
}
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
context.contentTimestamp(forKey: key)
}
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
context.recordContentKey(key, timestamp: timestamp)
}
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
context.appendPublicMessage(message, to: conversationID)
}
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage) {
func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage) {
context.prewarmMessageFormatting(message)
}
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) {
func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool) {
context.setPublicBatching(isBatching)
}
}
@@ -135,7 +135,6 @@ final class ChatVerificationCoordinator {
let noiseKeyHex: String
let signKeyHex: String
let nonceA: Data
let startedAt: Date
var sent: Bool
}
@@ -258,7 +257,6 @@ final class ChatVerificationCoordinator {
noiseKeyHex: qr.noiseKeyHex,
signKeyHex: qr.signKeyHex,
nonceA: nonce,
startedAt: Date(),
sent: false
)
pendingQRVerifications[peerID] = pending
+1 -51
View File
@@ -84,7 +84,6 @@ import SwiftUI
import Combine
import CommonCrypto
import CoreBluetooth
import Tor
#if os(iOS)
import UIKit
#endif
@@ -222,12 +221,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
conversations.unreadDirectRoutingPeerIDs()
}
/// Check if there are any unread messages (including from temporary Nostr peer IDs)
@MainActor
var hasAnyUnreadMessages: Bool {
!unreadPrivateMessages.isEmpty
}
/// Open the most relevant private chat when tapping the toolbar unread icon.
/// Prefers the most recently active unread conversation, otherwise the most recent PM.
@MainActor
@@ -245,20 +238,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
set { peerIdentityStore.setSelectedPrivateChatFingerprint(newValue) }
}
// Resolve full Noise key for a peer's short ID (used by UI header rendering)
@MainActor
private func getNoiseKeyForShortID(_ shortPeerID: PeerID) -> PeerID? {
if let mapped = peerIdentityStore.stablePeerID(forShortID: shortPeerID) { return mapped }
// Fallback: derive from active Noise session if available
if shortPeerID.id.count == 16,
let key = meshService.noiseSessionPublicKeyData(for: shortPeerID) {
let stable = PeerID(hexData: key)
peerIdentityStore.setStablePeerID(stable, forShortID: shortPeerID)
return stable
}
return nil
}
// Resolve short mesh ID (16-hex) from a full Noise public key hex (64-hex)
@MainActor
func getShortIDForNoiseKey(_ fullNoiseKeyHex: PeerID) -> PeerID {
@@ -354,18 +333,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// MARK: - Social Features (Delegated to PeerStateManager)
@MainActor
var favoritePeers: Set<String> { unifiedPeerService.favoritePeers }
@MainActor
var blockedUsers: Set<String> { unifiedPeerService.blockedUsers }
// MARK: - Encryption and Security
// Noise Protocol encryption status
var peerEncryptionStatus: [PeerID: EncryptionStatus] {
get { peerIdentityStore.encryptionStatuses }
set { peerIdentityStore.replaceEncryptionStatuses(newValue) }
}
var verifiedFingerprints: Set<String> {
get { peerIdentityStore.verifiedFingerprints }
set { peerIdentityStore.setVerifiedFingerprints(newValue) }
@@ -959,11 +931,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// MARK: - Public helpers
/// Published geohash people list for SwiftUI observation
var geohashPeople: [GeoPerson] {
participantTracker.visiblePeople
}
/// Return the current, pruned, sorted people list for the active geohash without mutating state.
@MainActor
func visibleGeohashPeople() -> [GeoPerson] {
@@ -1056,14 +1023,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
publicConversationCoordinator.displayNameForNostrPubkey(pubkeyHex)
}
// MARK: - Media Transfers
private enum MediaSendError: Error {
case encodingFailed
case tooLarge
case copyFailed
}
func currentPublicSender() -> (name: String, peerID: PeerID) {
publicConversationCoordinator.currentPublicSender()
}
@@ -1125,7 +1084,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
}
@MainActor
@objc func handlePeerStatusUpdate(_ notification: Notification) {
@objc func handlePeerStatusUpdate(_: Notification) {
peerIdentityCoordinator.handlePeerStatusUpdate()
}
@@ -1159,15 +1118,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
lifecycleCoordinator.markPrivateMessagesAsRead(from: peerID)
}
func getMessages(for peerID: PeerID?) -> [BitchatMessage] {
lifecycleCoordinator.getMessages(for: peerID)
}
@MainActor
func getPrivateChatMessages(for peerID: PeerID) -> [BitchatMessage] {
lifecycleCoordinator.getPrivateChatMessages(for: peerID)
}
@MainActor
func getPeerIDForNickname(_ nickname: String) -> PeerID? {
peerIdentityCoordinator.getPeerIDForNickname(nickname)
@@ -35,11 +35,6 @@ extension ChatViewModel {
nostrCoordinator.inbound.handleNostrEvent(event)
}
@MainActor
func subscribeToGeoChat(_ ch: GeohashChannel) {
nostrCoordinator.subscriptions.subscribeToGeoChat(ch)
}
@MainActor
func handleGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) {
nostrCoordinator.inbound.handleGiftWrap(giftWrap, id: id)
@@ -55,21 +50,11 @@ extension ChatViewModel {
nostrCoordinator.subscriptions.beginGeohashSampling(for: geohashes)
}
@MainActor
func subscribe(_ gh: String) {
nostrCoordinator.subscriptions.subscribe(gh)
}
@MainActor
func subscribeNostrEvent(_ event: NostrEvent, gh: String) {
nostrCoordinator.presence.subscribeNostrEvent(event, gh: gh)
}
@MainActor
func cooldownPerGeohash(_ gh: String, content: String, event: NostrEvent) {
nostrCoordinator.presence.cooldownPerGeohash(gh, content: content, event: event)
}
@MainActor
func endGeohashSampling() {
nostrCoordinator.subscriptions.endGeohashSampling()
@@ -80,35 +65,11 @@ extension ChatViewModel {
nostrCoordinator.subscriptions.setupNostrMessageHandling()
}
@MainActor
func handleNostrMessage(_ giftWrap: NostrEvent) {
nostrCoordinator.inbound.handleNostrMessage(giftWrap)
}
func processNostrMessage(_ giftWrap: NostrEvent) async {
await nostrCoordinator.inbound.processNostrMessage(giftWrap)
}
@MainActor
func findNoiseKey(for nostrPubkey: String) -> Data? {
nostrCoordinator.inbound.findNoiseKey(for: nostrPubkey)
}
@MainActor
func sendDeliveryAckViaNostrEmbedded(
_ message: BitchatMessage,
wasReadBefore: Bool,
senderPubkey: String,
key: Data?
) {
nostrCoordinator.sendDeliveryAckViaNostrEmbedded(
message,
wasReadBefore: wasReadBefore,
senderPubkey: senderPubkey,
key: key
)
}
@MainActor
func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
nostrCoordinator.sendFavoriteNotificationViaNostr(noisePublicKey: noisePublicKey, isFavorite: isFavorite)
@@ -55,16 +55,6 @@ extension ChatViewModel {
privateConversationCoordinator.handleReadReceipt(payload, senderPubkey: senderPubkey, convKey: convKey)
}
@MainActor
func sendDeliveryAckIfNeeded(to messageId: String, senderPubKey: String, from id: NostrIdentity) {
privateConversationCoordinator.sendDeliveryAckIfNeeded(to: messageId, senderPubKey: senderPubKey, from: id)
}
@MainActor
func sendReadReceiptIfNeeded(to messageId: String, senderPubKey: String, from id: NostrIdentity) {
privateConversationCoordinator.sendReadReceiptIfNeeded(to: messageId, senderPubKey: senderPubKey, from: id)
}
@MainActor
func sendVoiceNote(at url: URL) {
mediaTransferCoordinator.sendVoiceNote(at: url)
@@ -162,11 +152,6 @@ extension ChatViewModel {
mediaTransferCoordinator.clearTransferMapping(for: messageID)
}
@MainActor
func handleMediaSendFailure(messageID: String, reason: String) {
mediaTransferCoordinator.handleMediaSendFailure(messageID: messageID, reason: reason)
}
@MainActor
func handleTransferEvent(_ event: TransferProgressManager.Event) {
mediaTransferCoordinator.handleTransferEvent(event)
@@ -194,59 +179,6 @@ extension ChatViewModel {
privateConversationCoordinator.handlePrivateMessage(message)
}
@MainActor
func isDuplicateMessage(_ messageId: String, targetPeerID: PeerID) -> Bool {
privateConversationCoordinator.isDuplicateMessage(messageId, targetPeerID: targetPeerID)
}
@MainActor
func addMessageToPrivateChatsIfNeeded(_ message: BitchatMessage, targetPeerID: PeerID) {
privateConversationCoordinator.addMessageToPrivateChatsIfNeeded(message, targetPeerID: targetPeerID)
}
@MainActor
func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: PeerID, key: Data?) {
privateConversationCoordinator.mirrorToEphemeralIfNeeded(message, targetPeerID: targetPeerID, key: key)
}
@MainActor
func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: PeerID, key: Data?, senderPubkey: String) {
privateConversationCoordinator.handleViewingThisChat(
message,
targetPeerID: targetPeerID,
key: key,
senderPubkey: senderPubkey
)
}
@MainActor
func markAsUnreadIfNeeded(
shouldMarkAsUnread: Bool,
targetPeerID: PeerID,
key: Data?,
isRecentMessage: Bool,
senderNickname: String,
messageContent: String
) {
privateConversationCoordinator.markAsUnreadIfNeeded(
shouldMarkAsUnread: shouldMarkAsUnread,
targetPeerID: targetPeerID,
key: key,
isRecentMessage: isRecentMessage,
senderNickname: senderNickname,
messageContent: messageContent
)
}
@MainActor
func handleFavoriteNotification(_ content: String, from peerID: PeerID, senderNickname: String) {
privateConversationCoordinator.handleFavoriteNotification(
content,
from: peerID,
senderNickname: senderNickname
)
}
@MainActor
func processActionMessage(_ message: BitchatMessage) -> BitchatMessage {
privateConversationCoordinator.processActionMessage(message)
@@ -257,11 +189,6 @@ extension ChatViewModel {
privateConversationCoordinator.migratePrivateChatsIfNeeded(for: peerID, senderNickname: senderNickname)
}
@MainActor
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
privateConversationCoordinator.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
}
@MainActor
func isMessageBlocked(_ message: BitchatMessage) -> Bool {
privateConversationCoordinator.isMessageBlocked(message)
@@ -54,7 +54,7 @@ extension ChatViewModel {
}
}
@objc func handleTorPreferenceChanged(_ notification: Notification) {
@objc func handleTorPreferenceChanged(_: Notification) {
Task { @MainActor in
self.torStatusAnnounced = false
self.torInitialReadyAnnounced = false
@@ -79,9 +79,4 @@ struct MessageRateLimiter {
return senderAllowed && contentAllowed
}
mutating func reset() {
senderBuckets.removeAll()
contentBuckets.removeAll()
}
}
@@ -82,13 +82,6 @@ final class MinimalDistancePalette {
return Color(hue: entry.hue, saturation: saturation, brightness: brightness)
}
@MainActor
func reset() {
currentSeeds.removeAll()
entries.removeAll()
previousEntries.removeAll()
}
@MainActor
private func rebuildEntries() {
guard !currentSeeds.isEmpty else {
@@ -14,15 +14,15 @@ import Foundation
@MainActor
protocol PublicMessagePipelineDelegate: AnyObject {
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date?
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date)
func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String
func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date?
func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date)
/// Commits a batched message to its conversation in the store.
/// Returns `false` when the message was already present (ID dedup).
@discardableResult
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage)
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool)
func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool
func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage)
func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool)
}
@MainActor
-2
View File
@@ -15,8 +15,6 @@ struct AppInfoView: View {
AppTheme(rawValue: appThemeRawValue) ?? .matrix
}
private var backgroundColor: Color { palette.background }
private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary }
+1 -7
View File
@@ -11,8 +11,6 @@ struct ContentPeopleSheetView: View {
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
@EnvironmentObject private var verificationModel: VerificationModel
@EnvironmentObject private var conversationUIModel: ConversationUIModel
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@EnvironmentObject private var peerListModel: PeerListModel
@Binding var showSidebar: Bool
@Binding var messageText: String
@@ -79,8 +77,7 @@ struct ContentPeopleSheetView: View {
#endif
} else {
ContentPeopleListView(
showSidebar: $showSidebar,
headerHeight: headerHeight
showSidebar: $showSidebar
)
}
}
@@ -142,8 +139,6 @@ private struct ContentPeopleListView: View {
@Binding var showSidebar: Bool
let headerHeight: CGFloat
@State private var showVerifySheet = false
var body: some View {
@@ -285,7 +280,6 @@ private extension ContentPeopleListView {
}
private struct ContentPrivateChatSheetView: View {
@EnvironmentObject private var appChromeModel: AppChromeModel
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
@Binding var showSidebar: Bool
-5
View File
@@ -17,8 +17,6 @@ struct FingerprintView: View {
private var textColor: Color { palette.primary }
private var backgroundColor: Color { palette.background }
private enum Strings {
static let title: LocalizedStringKey = "fingerprint.title"
static let theirFingerprint: LocalizedStringKey = "fingerprint.their_label"
@@ -45,9 +43,6 @@ struct FingerprintView: View {
count
)
}
static func unknownPeer() -> String {
String(localized: "common.unknown", comment: "Label for an unknown peer")
}
}
var body: some View {
+1 -3
View File
@@ -13,8 +13,6 @@ struct LocationChannelsSheet: View {
@State private var customGeohash: String = ""
@State private var customError: String? = nil
private var backgroundColor: Color { palette.background }
private enum Strings {
static let title: LocalizedStringKey = "location_channels.title"
static let description: LocalizedStringKey = "location_channels.description"
@@ -401,7 +399,7 @@ struct LocationChannelsSheet: View {
title: String,
subtitlePrefix: String,
subtitleName: String? = nil,
subtitleNameBold: Bool = false,
subtitleNameBold _: Bool = false,
isSelected: Bool,
titleColor: Color? = nil,
titleBold: Bool = false,
-1
View File
@@ -288,7 +288,6 @@ struct VerificationSheetView: View {
@State private var showingScanner = false
@ThemedPalette private var palette
private var backgroundColor: Color { palette.background }
private var accentColor: Color { palette.accent }
private var boxColor: Color { palette.secondary.opacity(0.1) }
@@ -70,7 +70,6 @@ private final class MockChatNostrContext: ChatNostrContext {
private(set) var mentionCheckedMessageIDs: [String] = []
private(set) var hapticMessageIDs: [String] = []
func handlePublicMessage(_ message: BitchatMessage) { handledPublicMessages.append(message) }
func handlePublicMessage(_ message: BitchatMessage, powBits: Int) { handledPublicMessages.append(message) }
func checkForMentions(_ message: BitchatMessage) { mentionCheckedMessageIDs.append(message.id) }
func sendHapticFeedback(for message: BitchatMessage) { hapticMessageIDs.append(message.id) }
@@ -214,8 +213,6 @@ private final class MockChatNostrContext: ChatNostrContext {
// Favorites & notifications
var favoriteRelationshipsByNoiseKey: [Data: FavoritesPersistenceService.FavoriteRelationship] = [:]
private(set) var addedFavorites: [(noiseKey: Data, nostrPublicKey: String?, nickname: String)] = []
private(set) var postedLocalNotifications: [(title: String, body: String, identifier: String)] = []
private(set) var geohashActivityNotifications: [(geohash: String, bodyPreview: String)] = []
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship? {
@@ -226,14 +223,6 @@ private final class MockChatNostrContext: ChatNostrContext {
Array(favoriteRelationshipsByNoiseKey.values)
}
func addFavorite(noiseKey: Data, nostrPublicKey: String?, nickname: String) {
addedFavorites.append((noiseKey, nostrPublicKey, nickname))
}
func postLocalNotification(title: String, body: String, identifier: String) {
postedLocalNotifications.append((title, body, identifier))
}
func notifyGeohashActivity(geohash: String, bodyPreview: String) {
geohashActivityNotifications.append((geohash, bodyPreview))
}
@@ -250,24 +239,6 @@ private func drainMainQueue() async {
}
}
private func makeFavoriteRelationship(
noiseKey: Data,
nostrPublicKey: String? = nil,
nickname: String = "alice",
isFavorite: Bool = false,
theyFavoritedUs: Bool = false
) -> FavoritesPersistenceService.FavoriteRelationship {
FavoritesPersistenceService.FavoriteRelationship(
peerNoisePublicKey: noiseKey,
peerNostrPublicKey: nostrPublicKey,
peerNickname: nickname,
isFavorite: isFavorite,
theyFavoritedUs: theyFavoritedUs,
favoritedAt: Date(timeIntervalSince1970: 0),
lastUpdated: Date(timeIntervalSince1970: 0)
)
}
// MARK: - Coordinator Tests Against Mock Context
/// Exercises `ChatNostrCoordinator` against `MockChatNostrContext` with no
@@ -29,7 +29,6 @@ private final class MockChatPeerIdentityContext: ChatPeerIdentityContext {
var unreadPrivateMessages: Set<PeerID> = []
var selectedPrivateChatPeer: PeerID?
var selectedPrivateChatFingerprint: String?
var nickname = "me"
var myPeerID = PeerID(str: "0011223344556677")
var activeChannel: ChannelID = .mesh
private(set) var notifyUIChangedCount = 0
@@ -100,11 +100,6 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
unreadPrivateMessages.remove(peerID)
}
func removePrivateChat(_ peerID: PeerID) {
privateChats.removeValue(forKey: peerID)
unreadPrivateMessages.remove(peerID)
}
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID) {
migratedChats.append((oldPeerID, newPeerID))
guard oldPeerID != newPeerID, let source = privateChats[oldPeerID] else { return }
@@ -177,12 +172,10 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
// Routing & acknowledgements
private(set) var routedPrivateMessages: [(content: String, peerID: PeerID, messageID: String)] = []
private(set) var routedReadReceipts: [(messageID: String, peerID: PeerID)] = []
private(set) var routedFavoriteNotifications: [(peerID: PeerID, isFavorite: Bool)] = []
private(set) var meshReadReceipts: [(messageID: String, peerID: PeerID)] = []
private(set) var geoPrivateMessages: [(content: String, recipientHex: String, messageID: String)] = []
private(set) var geoDeliveryAcks: [(messageID: String, recipientHex: String)] = []
private(set) var geoReadReceipts: [(messageID: String, recipientHex: String)] = []
private(set) var embeddedDeliveryAckMessageIDs: [String] = []
func routePrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
routedPrivateMessages.append((content, peerID, messageID))
@@ -192,10 +185,6 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
routedReadReceipts.append((receipt.originalMessageID, peerID))
}
func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
routedFavoriteNotifications.append((peerID, isFavorite))
}
func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
meshReadReceipts.append((receipt.originalMessageID, peerID))
}
@@ -212,10 +201,6 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
geoReadReceipts.append((messageID, recipientHex))
}
func sendDeliveryAckViaNostrEmbedded(_ message: BitchatMessage, wasReadBefore: Bool, senderPubkey: String, key: Data?) {
embeddedDeliveryAckMessageIDs.append(message.id)
}
// Favorites & notifications
var favoriteRelationshipsByNoiseKey: [Data: FavoritesPersistenceService.FavoriteRelationship] = [:]
private(set) var peerFavoritedUsUpdates: [(noiseKey: Data, favorited: Bool, nickname: String, nostrPublicKey: String?)] = []
@@ -58,11 +58,6 @@ private final class MockChatPublicConversationContext: ChatPublicConversationCon
return true
}
@discardableResult
func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool {
appendPublicMessage(message, to: .geohash(geohash.lowercased()))
}
func publicConversationContainsMessage(withID messageID: String, in conversationID: ConversationID) -> Bool {
conversations[conversationID]?.contains(where: { $0.id == messageID }) == true
}
-4
View File
@@ -675,10 +675,6 @@ private final class MockCommandContextProvider: CommandContextProvider {
toggledFavorites.append(peerID)
}
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
favoriteNotifications.append((peerID, isFavorite))
}
// Groups: record the parsed subcommand + argument the processor forwarded.
private(set) var groupCommands: [(subcommand: String, argument: String)] = []
@@ -590,9 +590,6 @@ private final class CourierCaptureTransport: Transport {
private(set) var courierSends: [(messageID: String, recipientKey: Data, couriers: [PeerID])] = []
private(set) var directSends: [String] = []
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
Just(snapshots).eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { snapshots }
var myPeerID = PeerID(str: "00000000000000aa")
@@ -19,8 +19,6 @@ struct PublicChatE2ETests {
private let david: MockBLEService
private let bus = MockBLEBus()
private var receivedMessages: [String: [BitchatMessage]] = [:]
init() {
// Create mock services with unique peer IDs to avoid any collision
alice = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname1, bus: bus)
-1
View File
@@ -1,6 +1,5 @@
import SwiftUI
import XCTest
@testable import bitchat
final class FontBitchatTests: XCTestCase {
// func testMonospacedMapping() {
@@ -369,7 +369,6 @@ extension FragmentationTests {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
func didUpdateBluetoothState(_ state: CBManagerState) {}
func didReceiveRegionalPublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {}
}
// Helper: build a large message packet (unencrypted public message)
@@ -33,14 +33,6 @@ final class TestNetworkHelper {
return node
}
func getNode(_ name: String) -> MockBLEService? {
nodes[name]
}
func getManager(_ name: String) -> NoiseSessionManager? {
noiseManagers[name]
}
// MARK: - Topology
func connect(_ a: String, _ b: String) {
+1 -3
View File
@@ -10,7 +10,6 @@ struct LocalizationCoverageTests {
.deletingLastPathComponent() // repo root
private struct Catalog {
let sourceLanguage: String
/// key -> set of locales with a localization entry
let coverage: [String: Set<String>]
/// all locales appearing anywhere in the catalog
@@ -22,7 +21,6 @@ struct LocalizationCoverageTests {
let data = try Data(contentsOf: url)
let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
let strings = try #require(root["strings"] as? [String: Any])
let sourceLanguage = try #require(root["sourceLanguage"] as? String)
var coverage: [String: Set<String>] = [:]
for (key, value) in strings {
@@ -43,7 +41,7 @@ struct LocalizationCoverageTests {
}
coverage[key] = locales
}
return Catalog(sourceLanguage: sourceLanguage, coverage: coverage)
return Catalog(coverage: coverage)
}
@Test func mainCatalogCoversAllLocalesForEveryKey() throws {
-1
View File
@@ -8,7 +8,6 @@
import Foundation
import BitFoundation
@testable import bitchat
final class MockBLEBus {
private var registry: [PeerID: MockBLEService] = [:]
-62
View File
@@ -48,10 +48,6 @@ final class MockBLEService: NSObject {
set { myNickname = newValue }
}
var nickname: String {
return myNickname
}
var peerID: PeerID {
return myPeerID
}
@@ -62,12 +58,6 @@ final class MockBLEService: NSObject {
self.bus = bus
}
// MARK: - Methods matching BLEService
func setNickname(_ nickname: String) {
self.myNickname = nickname
}
// MARK: - In-memory test bus (for E2E/Integration)
/// Registers this instance on first use.
@@ -92,10 +82,6 @@ final class MockBLEService: NSObject {
return connectedPeers.contains(peerID)
}
func peerNickname(peerID: String) -> String? {
"MockPeer_\(peerID)"
}
func getPeerNicknames() -> [PeerID: String] {
var nicknames: [PeerID: String] = [:]
for peer in connectedPeers {
@@ -104,10 +90,6 @@ final class MockBLEService: NSObject {
return nicknames
}
func getPeers() -> [PeerID: String] {
return getPeerNicknames()
}
/// Keep local echo synchronous so Swift Testing confirmations observe it deterministically.
private func deliverLocalEcho(_ message: BitchatMessage) {
delegate?.didReceiveMessage(message)
@@ -155,14 +137,6 @@ final class MockBLEService: NSObject {
}
}
func sendFileBroadcast(_ packet: BitchatFilePacket, transferId: String) {
// Tests currently ignore file transfer flows; keep stub for protocol conformance.
}
func sendFilePrivate(_ packet: BitchatFilePacket, to peerID: PeerID, transferId: String) {
// Tests currently ignore file transfer flows; keep stub for protocol conformance.
}
func sendPrivateMessage(_ content: String, to recipientPeerID: PeerID, recipientNickname: String, messageID: String) {
let message = BitchatMessage(
id: messageID,
@@ -209,39 +183,6 @@ final class MockBLEService: NSObject {
}
}
func sendFavoriteNotification(to peerID: String, isFavorite: Bool) {
// Mock implementation
}
func sendReadReceipt(_ receipt: ReadReceipt, to peerID: String) {
// Mock implementation
}
func sendBroadcastAnnounce() {
// Mock implementation
}
func getPeerFingerprint(_ peerID: String) -> String? {
return nil
}
func getNoiseSessionState(for peerID: String) -> LazyHandshakeState {
return .none
}
func triggerHandshake(with peerID: String) {
// Mock implementation
}
func emergencyDisconnectAll() {
connectedPeers.removeAll()
delegate?.didUpdatePeerList([])
}
func getFingerprint(for peerID: String) -> String? {
return nil
}
// MARK: - Test Helper Methods
func simulateConnectedPeer(_ peerID: PeerID) {
@@ -314,9 +255,6 @@ final class MockBLEService: NSObject {
}
}
// Backward compatibility for older tests
typealias MockSimplifiedBluetoothService = MockBLEService
// MARK: - Helpers
extension MockBLEService {
+1 -20
View File
@@ -11,18 +11,11 @@ import BitFoundation
@testable import bitchat
final class MockIdentityManager: SecureIdentityStateManagerProtocol {
private let keychain: KeychainManagerProtocol
private var blockedFingerprints: Set<String> = []
private var blockedNostrPubkeys: Set<String> = []
private var socialIdentities: [String: SocialIdentity] = [:]
init(_ keychain: KeychainManagerProtocol) {
self.keychain = keychain
}
func loadIdentityCache() {}
func saveIdentityCache() {}
init(_: KeychainManagerProtocol) {}
func forceSave() {}
@@ -45,12 +38,6 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol {
}
}
func getFavorites() -> Set<String> {
Set()
}
func setFavorite(_ fingerprint: String, isFavorite: Bool) {}
func isFavorite(fingerprint: String) -> Bool {
false
}
@@ -100,8 +87,6 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol {
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
func updateHandshakeState(peerID: PeerID, state: HandshakeState) {}
func clearAllIdentityData() {}
func removeEphemeralSession(peerID: PeerID) {}
@@ -139,10 +124,6 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol {
!(vouchesByVouchee[fingerprint] ?? []).isEmpty
}
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel {
socialIdentities[fingerprint]?.trustLevel ?? .unknown
}
func lastVouchBatchSent(to fingerprint: String) -> Date? {
vouchBatchSentAt[fingerprint]
}
-3
View File
@@ -26,9 +26,6 @@ final class MockTransport: Transport {
var myNickname: String = "TestUser"
private let peerSnapshotSubject = CurrentValueSubject<[TransportPeerSnapshot], Never>([])
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
peerSnapshotSubject.eraseToAnyPublisher()
}
// MARK: - Recording Properties (for test assertions)
@@ -31,11 +31,8 @@ struct NoiseTestVector: Codable {
let init_prologue: String
let init_static: String
let init_ephemeral: String
let init_psks: [String]?
let resp_prologue: String
let resp_static: String
let resp_ephemeral: String
let resp_psks: [String]?
let handshake_hash: String?
let messages: [TestMessage]
+1
View File
@@ -156,6 +156,7 @@ struct NostrProtocolTests {
}
}
@Test
func testAckRoundTripNIP44V2_Delivered() throws {
// Identities
let sender = try NostrIdentity.generate()
@@ -118,6 +118,7 @@ struct NotificationStreamAssemblerTests {
#expect(decoded.timestamp == packet2.timestamp)
}
@Test
func testAssemblesCompressedLargeFrame() throws {
var assembler = NotificationStreamAssembler()
@@ -610,7 +610,6 @@ private final class PerfNostrContext: ChatNostrContext {
func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool { true }
private(set) var handledPublicMessageCount = 0
func handlePublicMessage(_ message: BitchatMessage) { handledPublicMessageCount += 1 }
func handlePublicMessage(_ message: BitchatMessage, powBits: Int) { handledPublicMessageCount += 1 }
func checkForMentions(_ message: BitchatMessage) {}
func sendHapticFeedback(for message: BitchatMessage) {}
@@ -668,8 +667,6 @@ private final class PerfNostrContext: ChatNostrContext {
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship? { nil }
func allFavoriteRelationships() -> [FavoritesPersistenceService.FavoriteRelationship] { [] }
func addFavorite(noiseKey: Data, nostrPublicKey: String?, nickname: String) {}
func postLocalNotification(title: String, body: String, identifier: String) {}
func notifyGeohashActivity(geohash: String, bodyPreview: String) {}
}
@@ -779,7 +776,6 @@ private final class PerfDeliveryContext: ChatDeliveryContext {
@MainActor
private final class PerfPipelineFixture {
let viewModel: ChatViewModel
let transport: MockTransport
let conversations: ConversationStore
let privateInbox: PrivateInboxModel
let publicChat: PublicChatModel
@@ -791,7 +787,6 @@ private final class PerfPipelineFixture {
let transport = MockTransport()
let conversations = ConversationStore()
self.transport = transport
self.conversations = conversations
self.viewModel = ChatViewModel(
keychain: keychain,
-4
View File
@@ -23,10 +23,6 @@ private final class DefaultTransportProbe: Transport {
var myNickname = "Tester"
private(set) var sentMessages: [(content: String, mentions: [String])] = []
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
subject.eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { subject.value }
func setNickname(_ nickname: String) { myNickname = nickname }
func startServices() {}
@@ -27,28 +27,28 @@ private final class TestPipelineDelegate: PublicMessagePipelineDelegate {
committed.filter { $0.conversationID == conversationID }.map(\.message)
}
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String {
func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String {
dedupService.normalizedContentKey(content)
}
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
dedupService.contentTimestamp(forKey: key)
}
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
dedupService.recordContentKey(key, timestamp: timestamp)
recordedContentKeys.append(key)
}
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
guard !rejectedMessageIDs.contains(message.id) else { return false }
committed.append((message, conversationID))
return true
}
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage) {}
func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage) {}
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) {
func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool) {
batchingStates.append(isBatching)
}
}
@@ -230,8 +230,4 @@ private final class MockGeohashPresenceTimer: GeohashPresenceTimerProtocol {
invalidateCallCount += 1
isValid = false
}
func fire() {
handler()
}
}
@@ -120,7 +120,6 @@ final class NetworkActivationServiceTests: XCTestCase {
return NetworkActivationTestContext(
service: service,
storage: storage,
permissionSubject: permissionSubject,
favoritesSubject: favoritesSubject,
torController: torController,
relayController: relayController,
@@ -148,7 +147,6 @@ final class NetworkActivationServiceTests: XCTestCase {
private struct NetworkActivationTestContext {
let service: NetworkActivationService
let storage: UserDefaults
let permissionSubject: CurrentValueSubject<LocationChannelManager.PermissionState, Never>
let favoritesSubject: CurrentValueSubject<Set<Data>, Never>
let torController: MockNetworkActivationTorController
let relayController: MockNetworkActivationRelayController
@@ -1483,7 +1483,6 @@ final class NostrRelayManagerTests: XCTestCase {
return RelayManagerTestContext(
manager: manager,
permissionSubject: permissionSubject,
favoritesSubject: favoritesSubject,
sessionFactory: sessionFactory,
scheduler: scheduler,
clock: clock,
@@ -1537,7 +1536,6 @@ final class NostrRelayManagerTests: XCTestCase {
private struct RelayManagerTestContext {
let manager: NostrRelayManager
let permissionSubject: CurrentValueSubject<LocationChannelManager.PermissionState, Never>
let favoritesSubject: CurrentValueSubject<Set<Data>, Never>
let sessionFactory: MockRelaySessionFactory
let scheduler: MockRelayScheduler
let clock: MutableClock
@@ -1644,7 +1642,6 @@ private final class MockRelaySessionFactory: NostrRelaySessionProtocol {
}
private final class MockRelayConnection: NostrRelayConnectionProtocol {
private let url: String
private let pingError: Error?
private let sendError: Error?
private var receiveHandler: ((Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
@@ -1662,8 +1659,7 @@ private final class MockRelayConnection: NostrRelayConnectionProtocol {
}
}
init(url: String, pingError: Error? = nil, sendError: Error? = nil) {
self.url = url
init(url _: String, pingError: Error? = nil, sendError: Error? = nil) {
self.pingError = pingError
self.sendError = sendError
}
@@ -214,18 +214,6 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
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)
}
@@ -266,8 +254,6 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
func updateHandshakeState(peerID: PeerID, state: HandshakeState) {}
func clearAllIdentityData() {
socialIdentities.removeAll()
favorites.removeAll()
@@ -308,10 +294,6 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
false
}
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel {
verified.contains(fingerprint) ? .verified : .unknown
}
func lastVouchBatchSent(to fingerprint: String) -> Date? {
nil
}
@@ -25,9 +25,5 @@ struct TestConstants {
static let testNickname4 = "David"
static let testMessage1 = "Hello, World!"
static let testMessage2 = "How are you?"
static let testMessage3 = "This is a test message"
static let testLongMessage = String(repeating: "This is a long message. ", count: 100)
static let testSignature = Data(repeating: 0xAB, count: 64)
}
@@ -13,19 +13,6 @@ import BitFoundation
final class TestHelpers {
// MARK: - Key Generation
static func generateTestKeyPair() -> (privateKey: Curve25519.KeyAgreement.PrivateKey, publicKey: Curve25519.KeyAgreement.PublicKey) {
let privateKey = Curve25519.KeyAgreement.PrivateKey()
let publicKey = privateKey.publicKey
return (privateKey, publicKey)
}
static func generateTestIdentity(peerID: String, nickname: String) -> (peerID: String, nickname: String, privateKey: Curve25519.KeyAgreement.PrivateKey, publicKey: Curve25519.KeyAgreement.PublicKey) {
let (privateKey, publicKey) = generateTestKeyPair()
return (peerID: peerID, nickname: nickname, privateKey: privateKey, publicKey: publicKey)
}
// MARK: - Message Creation
static func createTestMessage(
@@ -79,10 +66,6 @@ final class TestHelpers {
return data
}
static func generateTestPeerID() -> String {
return "PEER" + UUID().uuidString.prefix(8)
}
// MARK: - Async Helpers
static func waitFor(_ condition: @escaping () -> Bool, timeout: TimeInterval = TestConstants.defaultTimeout) async throws {
@@ -110,32 +93,10 @@ final class TestHelpers {
}
return true
}
static func expectAsync<T>(
timeout: TimeInterval = TestConstants.defaultTimeout,
operation: @escaping () async throws -> T
) async throws -> T {
return try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
return try await operation()
}
group.addTask {
try await sleep(1)
throw TestError.timeout
}
let result = try await group.next()!
group.cancelAll()
return result
}
}
}
enum TestError: Error {
case timeout
case unexpectedValue
case testFailure(String)
}
// MARK: - Private chat seeding (ConversationStore migration)
@@ -164,18 +125,6 @@ extension ChatViewModel {
}
}
/// Test-only replacement for `messages.removeAll()`: empties a public
/// channel's conversation.
@MainActor
func clearPublicMessages(for channel: ChannelID = .mesh) {
conversations.clear(ConversationID(channelID: channel))
}
/// Test-only: drops every private chat and unread flag.
@MainActor
func clearAllPrivateChats() {
conversations.removeAllDirectConversations()
}
}
func sleep(_ seconds: TimeInterval) async throws {
@@ -30,12 +30,6 @@ private func arti_bootstrap_progress() -> Int32
@_silgen_name("arti_bootstrap_summary")
private func arti_bootstrap_summary(_ buf: UnsafeMutablePointer<CChar>, _ len: Int32) -> Int32
@_silgen_name("arti_go_dormant")
private func arti_go_dormant() -> Int32
@_silgen_name("arti_wake")
private func arti_wake() -> Int32
/// Arti-based Tor integration for BitChat.
/// - Boots a local Arti client and exposes a SOCKS5 proxy
/// on 127.0.0.1:socksPort. All app networking should await readiness and
@@ -83,7 +77,6 @@ public final class TorManager: ObservableObject {
private var bootstrapMonitorStarted = false
private var pathMonitor: NWPathMonitor?
private var isAppForeground: Bool = true
private var isDormant: Bool = false
private var lastRestartAt: Date? = nil
private var startedAt: Date? = nil // Tracks initial startup time for grace period
private(set) var allowAutoStart: Bool = false
@@ -102,7 +95,6 @@ public final class TorManager: ObservableObject {
}
guard !didStart else { return }
didStart = true
isDormant = false
isStarting = true
startedAt = Date() // Track startup time for grace period
SecureLogger.debug("TorManager: startIfNeeded() - startedAt set", category: .session)
@@ -353,7 +345,6 @@ public final class TorManager: ObservableObject {
}
await MainActor.run {
self.isDormant = false
self.isReady = false
self.socksReady = false
self.bootstrapProgress = 0
@@ -383,7 +374,6 @@ public final class TorManager: ObservableObject {
self.bootstrapProgress = 0
self.bootstrapSummary = ""
self.isStarting = true
self.isDormant = false
self.lastRestartAt = Date()
}
@@ -105,10 +105,6 @@ public struct BinaryProtocol {
// Field offsets within packet header
public struct Offsets {
static let version = 0
static let type = 1
static let ttl = 2
static let timestamp = 3
public static let flags = 11 // After version(1) + type(1) + ttl(1) + timestamp(8)
}
@@ -341,21 +341,3 @@ extension BitchatMessage {
}
}
extension Array where Element == BitchatMessage {
/// Filters out empty ones and deduplicate by ID while preserving order (from oldest to newest)
public func cleanedAndDeduped() -> [Element] {
let arr = filter { $0.content.trimmed.isEmpty == false }
guard arr.count > 1 else {
return arr
}
var seen = Set<String>()
var dedup: [BitchatMessage] = []
for m in arr.sorted(by: { $0.timestamp < $1.timestamp }) {
if !seen.contains(m.id) {
dedup.append(m)
seen.insert(m.id)
}
}
return dedup
}
}
@@ -7,7 +7,6 @@
//
import struct Foundation.Data
import struct Foundation.Date
/// The core packet structure for all BitChat protocol messages.
/// Encapsulates all data needed for routing through the mesh network,
@@ -38,34 +37,6 @@ public struct BitchatPacket: Codable {
self.isRSR = isRSR
}
// Convenience initializer for new binary format
init(type: UInt8, ttl: UInt8, senderID: PeerID, payload: Data, isRSR: Bool = false) {
self.version = 1
self.type = type
// Convert hex string peer ID to binary data (8 bytes)
var senderData = Data()
var tempID = senderID.id
while tempID.count >= 2 {
let hexByte = String(tempID.prefix(2))
if let byte = UInt8(hexByte, radix: 16) {
senderData.append(byte)
}
tempID = String(tempID.dropFirst(2))
}
self.senderID = senderData
self.recipientID = nil
self.timestamp = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds
self.payload = payload
self.signature = nil
self.ttl = ttl
self.route = nil
self.isRSR = isRSR
}
var data: Data? {
BinaryProtocol.encode(self)
}
public func toBinaryData(padding: Bool = true) -> Data? {
BinaryProtocol.encode(self, padding: padding)
}

Some files were not shown because too many files have changed in this diff Show More