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 { actor AppEventStream {
private var continuations: [UUID: AsyncStream<AppEvent>.Continuation] = [:] 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) { func emit(_ event: AppEvent) {
for continuation in continuations.values { for continuation in continuations.values {
continuation.yield(event) 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 /// 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 /// (docs/CONVERSATION-STORE-DESIGN.md). Owned here; the feature models
/// and `ChatViewModel` observe and mutate it through its intent API. /// and `ChatViewModel` observe and mutate it through its intent API.
let conversations: ConversationStore let conversations: ConversationStore
let peerIdentityStore: PeerIdentityStore
let locationPresenceStore: LocationPresenceStore
let publicChatModel: PublicChatModel let publicChatModel: PublicChatModel
let privateInboxModel: PrivateInboxModel let privateInboxModel: PrivateInboxModel
let privateConversationModel: PrivateConversationModel let privateConversationModel: PrivateConversationModel
@@ -51,8 +49,6 @@ final class AppRuntime: ObservableObject {
let locationPresenceStore = LocationPresenceStore() let locationPresenceStore = LocationPresenceStore()
let locationManager = LocationChannelManager.shared let locationManager = LocationChannelManager.shared
self.conversations = conversations self.conversations = conversations
self.peerIdentityStore = peerIdentityStore
self.locationPresenceStore = locationPresenceStore
self.chatViewModel = ChatViewModel( self.chatViewModel = ChatViewModel(
keychain: keychain, keychain: keychain,
idBridge: idBridge, idBridge: idBridge,
-10
View File
@@ -796,16 +796,6 @@ extension ConversationStore {
return messageIDs 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 // MARK: - Diagnostics support
-2
View File
@@ -1,4 +1,3 @@
import BitFoundation
import Combine import Combine
import Foundation import Foundation
@@ -17,7 +16,6 @@ final class LocationChannelsModel: ObservableObject {
private let manager: LocationChannelManager private let manager: LocationChannelManager
private let network: NetworkActivationService private let network: NetworkActivationService
private let gateway: GatewayService private let gateway: GatewayService
private var cancellables = Set<AnyCancellable>()
init( init(
manager: LocationChannelManager? = nil, manager: LocationChannelManager? = nil,
-8
View File
@@ -25,10 +25,6 @@ final class PeerIdentityStore: ObservableObject {
stablePeerIDsByShortID[peerID] = stablePeerID stablePeerIDsByShortID[peerID] = stablePeerID
} }
func replaceStablePeerIDs(_ mappings: [PeerID: PeerID]) {
stablePeerIDsByShortID = mappings
}
func fingerprint(for peerID: PeerID) -> String? { func fingerprint(for peerID: PeerID) -> String? {
peerFingerprintsByPeerID[peerID] peerFingerprintsByPeerID[peerID]
} }
@@ -94,10 +90,6 @@ final class PeerIdentityStore: ObservableObject {
invalidateEncryptionCache(for: peerID) invalidateEncryptionCache(for: peerID)
} }
func replaceEncryptionStatuses(_ statuses: [PeerID: EncryptionStatus]) {
encryptionStatuses = statuses
}
func setVerifiedFingerprints(_ fingerprints: Set<String>) { func setVerifiedFingerprints(_ fingerprints: Set<String>) {
verifiedFingerprints = fingerprints verifiedFingerprints = fingerprints
} }
-6
View File
@@ -3,7 +3,6 @@ import Combine
import Foundation import Foundation
struct FingerprintPresentationState: Equatable { struct FingerprintPresentationState: Equatable {
let statusPeerID: PeerID
let peerNickname: String let peerNickname: String
let encryptionStatus: EncryptionStatus let encryptionStatus: EncryptionStatus
let theirFingerprint: String? let theirFingerprint: String?
@@ -56,10 +55,6 @@ final class VerificationModel: ObservableObject {
return VerificationService.shared.buildMyQRString(nickname: currentNickname, npub: npub) ?? "" return VerificationService.shared.buildMyQRString(nickname: currentNickname, npub: npub) ?? ""
} }
func beginQRVerification(with qr: VerificationService.VerificationQR) -> Bool {
chatViewModel.beginQRVerification(with: qr)
}
func verifyScannedPayload(_ payload: String) -> VerificationScanOutcome { func verifyScannedPayload(_ payload: String) -> VerificationScanOutcome {
guard let qr = VerificationService.shared.verifyScannedQR(payload) else { guard let qr = VerificationService.shared.verifyScannedQR(payload) else {
return .invalid return .invalid
@@ -110,7 +105,6 @@ final class VerificationModel: ObservableObject {
} }
return FingerprintPresentationState( return FingerprintPresentationState(
statusPeerID: statusPeerID,
peerNickname: peerNickname, peerNickname: peerNickname,
encryptionStatus: encryptionStatus, encryptionStatus: encryptionStatus,
theirFingerprint: theirFingerprint, theirFingerprint: theirFingerprint,
@@ -5,7 +5,6 @@ import AVFoundation
actor VoiceRecorder { actor VoiceRecorder {
enum RecorderError: Error { enum RecorderError: Error {
case microphoneAccessDenied case microphoneAccessDenied
case recorderInitializationFailed
case recordingInProgress 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]? { private func computeWaveform(url: URL, bins: Int) -> [Float]? {
guard bins > 0 else { return nil } guard bins > 0 else { return nil }
// Use autoreleasepool to manage memory from audio buffer allocations // 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. /// 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. /// These IDs rotate periodically to prevent tracking while maintaining cryptographic relationships.
struct EphemeralIdentity { struct EphemeralIdentity {
let peerID: PeerID // 8 random bytes
let sessionStart: Date
var handshakeState: HandshakeState var handshakeState: HandshakeState
} }
@@ -98,7 +96,6 @@ enum HandshakeState {
case initiated case initiated
case inProgress case inProgress
case completed(fingerprint: String) case completed(fingerprint: String)
case failed(reason: String)
} }
/// Represents the cryptographic layer of identity - the stable Noise Protocol static key pair. /// 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) // Optional Ed25519 signing public key (used to authenticate public messages)
var signingPublicKey: Data? = nil var signingPublicKey: Data? = nil
let firstSeen: Date let firstSeen: Date
let lastHandshake: Date?
} }
/// Represents the social layer of identity - user-assigned names and trust relationships. /// 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; // Fingerprint -> when we verified it (orders outgoing vouch batches;
// entries verified before this field exists sort as oldest) // entries verified before this field exists sort as oldest)
var verifiedAt: [String: Date]? = nil var verifiedAt: [String: Date]? = nil
// Schema version for future migrations
var version: Int = 1
} }
// //
@@ -108,8 +108,6 @@ protocol SecureIdentityStateManagerProtocol {
func updateSocialIdentity(_ identity: SocialIdentity) func updateSocialIdentity(_ identity: SocialIdentity)
// MARK: Favorites Management // MARK: Favorites Management
func getFavorites() -> Set<String>
func setFavorite(_ fingerprint: String, isFavorite: Bool)
func isFavorite(fingerprint: String) -> Bool func isFavorite(fingerprint: String) -> Bool
// MARK: Blocked Users Management // MARK: Blocked Users Management
@@ -123,8 +121,7 @@ protocol SecureIdentityStateManagerProtocol {
// MARK: Ephemeral Session Management // MARK: Ephemeral Session Management
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState)
func updateHandshakeState(peerID: PeerID, state: HandshakeState)
// MARK: Cleanup // MARK: Cleanup
func clearAllIdentityData() func clearAllIdentityData()
func removeEphemeralSession(peerID: PeerID) func removeEphemeralSession(peerID: PeerID)
@@ -139,7 +136,6 @@ protocol SecureIdentityStateManagerProtocol {
func recordVouch(voucheeFingerprint: String, voucherFingerprint: String, timestamp: Date) -> Bool func recordVouch(voucheeFingerprint: String, voucherFingerprint: String, timestamp: Date) -> Bool
func validVouchers(for fingerprint: String) -> [VouchRecord] func validVouchers(for fingerprint: String) -> [VouchRecord]
func isVouched(fingerprint: String) -> Bool func isVouched(fingerprint: String) -> Bool
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel
func lastVouchBatchSent(to fingerprint: String) -> Date? func lastVouchBatchSent(to fingerprint: String) -> Date?
func markVouchBatchSent(to fingerprint: String, at date: Date) func markVouchBatchSent(to fingerprint: String, at date: Date)
func signingPublicKey(forFingerprint fingerprint: String) -> Data? func signingPublicKey(forFingerprint fingerprint: String) -> Data?
@@ -322,21 +318,13 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
fingerprint: fingerprint, fingerprint: fingerprint,
publicKey: noisePublicKey, publicKey: noisePublicKey,
signingPublicKey: signingPublicKey ?? existing.signingPublicKey, signingPublicKey: signingPublicKey ?? existing.signingPublicKey,
firstSeen: existing.firstSeen, firstSeen: existing.firstSeen
lastHandshake: now
) )
self.cryptographicIdentities[fingerprint] = existing self.cryptographicIdentities[fingerprint] = existing
} else { } else {
// Update signing key and lastHandshake // Update signing key
existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey
let updated = CryptographicIdentity( self.cryptographicIdentities[fingerprint] = existing
fingerprint: existing.fingerprint,
publicKey: existing.publicKey,
signingPublicKey: existing.signingPublicKey,
firstSeen: existing.firstSeen,
lastHandshake: now
)
self.cryptographicIdentities[fingerprint] = updated
} }
// Persist updated state (already assigned in branches above) // Persist updated state (already assigned in branches above)
} else { } else {
@@ -345,8 +333,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
fingerprint: fingerprint, fingerprint: fingerprint,
publicKey: noisePublicKey, publicKey: noisePublicKey,
signingPublicKey: signingPublicKey, signingPublicKey: signingPublicKey,
firstSeen: now, firstSeen: now
lastHandshake: now
) )
self.cryptographicIdentities[fingerprint] = entry self.cryptographicIdentities[fingerprint] = entry
} }
@@ -511,11 +498,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState = .none) { func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState = .none) {
queue.async(flags: .barrier) { queue.async(flags: .barrier) {
self.ephemeralSessions[peerID] = EphemeralIdentity( self.ephemeralSessions[peerID] = EphemeralIdentity(handshakeState: handshakeState)
peerID: peerID,
sessionStart: Date(),
handshakeState: handshakeState
)
} }
} }
File diff suppressed because it is too large Load Diff
@@ -13,13 +13,6 @@ extension BitchatMessage {
enum Media { enum Media {
case voice(URL) case voice(URL)
case image(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 // 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 peerID: PeerID // Hex-encoded peer ID
let noisePublicKey: Data let noisePublicKey: Data
let nickname: String let nickname: String
let lastSeen: Date
let isConnected: Bool let isConnected: Bool
let isReachable: Bool let isReachable: Bool
@@ -77,14 +76,13 @@ struct BitchatPeer: Equatable {
peerID: PeerID, peerID: PeerID,
noisePublicKey: Data, noisePublicKey: Data,
nickname: String, nickname: String,
lastSeen: Date = Date(), lastSeen _: Date = Date(),
isConnected: Bool = false, isConnected: Bool = false,
isReachable: Bool = false isReachable: Bool = false
) { ) {
self.peerID = peerID self.peerID = peerID
self.noisePublicKey = noisePublicKey self.noisePublicKey = noisePublicKey
self.nickname = nickname self.nickname = nickname
self.lastSeen = lastSeen
self.isConnected = isConnected self.isConnected = isConnected
self.isReachable = isReachable self.isReachable = isReachable
+1 -1
View File
@@ -723,7 +723,7 @@ final class NoiseHandshakeState {
return messageBuffer 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 { guard currentPattern < messagePatterns.count else {
throw NoiseError.handshakeComplete throw NoiseError.handshakeComplete
@@ -21,12 +21,6 @@ enum NoiseSecurityConstants {
// Maximum number of messages before rekey (2^64 - 1 is the nonce limit) // Maximum number of messages before rekey (2^64 - 1 is the nonce limit)
static let maxMessagesPerSession: UInt64 = 1_000_000_000 // 1 billion messages 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 // Rate limiting
static let maxHandshakesPerMinute = 10 static let maxHandshakesPerMinute = 10
static let maxMessagesPerSecond = 100 static let maxMessagesPerSecond = 100
-1
View File
@@ -14,5 +14,4 @@ enum NoiseSecurityError: Error {
case messageTooLarge case messageTooLarge
case invalidPeerID case invalidPeerID
case rateLimitExceeded case rateLimitExceeded
case handshakeTimeout
} }
+2 -8
View File
@@ -13,8 +13,6 @@ import BitFoundation
final class NoiseSessionManager { final class NoiseSessionManager {
private var sessions: [PeerID: NoiseSession] = [:] private var sessions: [PeerID: NoiseSession] = [:]
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
private let keychain: KeychainManagerProtocol
private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
@@ -23,8 +21,6 @@ final class NoiseSessionManager {
var onSessionFailed: ((PeerID, Error) -> Void)? var onSessionFailed: ((PeerID, Error) -> Void)?
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) { init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
self.localStaticKey = localStaticKey
self.keychain = keychain
self.sessionFactory = { peerID, role in self.sessionFactory = { peerID, role in
SecureNoiseSession( SecureNoiseSession(
peerID: peerID, peerID: peerID,
@@ -37,12 +33,10 @@ final class NoiseSessionManager {
#if DEBUG #if DEBUG
init( init(
localStaticKey: Curve25519.KeyAgreement.PrivateKey, localStaticKey _: Curve25519.KeyAgreement.PrivateKey,
keychain: KeychainManagerProtocol, keychain _: KeychainManagerProtocol,
sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession
) { ) {
self.localStaticKey = localStaticKey
self.keychain = keychain
self.sessionFactory = sessionFactory self.sessionFactory = sessionFactory
} }
#endif #endif
+2 -10
View File
@@ -6,14 +6,12 @@ struct NostrIdentity: Codable {
let privateKey: Data let privateKey: Data
let publicKey: Data let publicKey: Data
let npub: String // Bech32-encoded public key let npub: String // Bech32-encoded public key
let createdAt: Date
/// Memberwise initializer /// Memberwise initializer
init(privateKey: Data, publicKey: Data, npub: String, createdAt: Date) { init(privateKey: Data, publicKey: Data, npub: String, createdAt _: Date) {
self.privateKey = privateKey self.privateKey = privateKey
self.publicKey = publicKey self.publicKey = publicKey
self.npub = npub self.npub = npub
self.createdAt = createdAt
} }
/// Generate a new Nostr identity /// Generate a new Nostr identity
@@ -39,12 +37,6 @@ struct NostrIdentity: Codable {
self.privateKey = privateKeyData self.privateKey = privateKeyData
self.publicKey = xOnlyPubkey self.publicKey = xOnlyPubkey
self.npub = try Bech32.encode(hrp: "npub", data: 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 /// Get Schnorr signing key for Nostr event signatures
-8
View File
@@ -37,14 +37,6 @@ final class NostrIdentityBridge {
return nostrIdentity 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 /// Get Nostr public key associated with a Noise public key
func getNostrPublicKey(for noisePublicKey: Data) -> String? { func getNostrPublicKey(for noisePublicKey: Data) -> String? {
let key = "nostr-noise-\(noisePublicKey.base64EncodedString())" let key = "nostr-noise-\(noisePublicKey.base64EncodedString())"
-34
View File
@@ -548,37 +548,6 @@ struct NostrProtocol {
return sharedSecretData 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 { private static func randomizedTimestamp() -> Date {
// Add random offset to current time for privacy // Add random offset to current time for privacy
// This prevents timing correlation attacks while the actual message timestamp // This prevents timing correlation attacks while the actual message timestamp
@@ -708,11 +677,8 @@ struct NostrEvent: Codable {
enum NostrError: Error { enum NostrError: Error {
case invalidPublicKey case invalidPublicKey
case invalidPrivateKey
case invalidEvent case invalidEvent
case invalidCiphertext case invalidCiphertext
case signingFailed
case encryptionFailed
} }
// MARK: - NIP-44 v2 helpers (XChaCha20-Poly1305) // MARK: - NIP-44 v2 helpers (XChaCha20-Poly1305)
-3
View File
@@ -117,7 +117,6 @@ final class NostrRelayManager: ObservableObject {
let url: String let url: String
var isConnected: Bool = false var isConnected: Bool = false
var lastError: Error? var lastError: Error?
var lastConnectedAt: Date?
var messagesSent: Int = 0 var messagesSent: Int = 0
var messagesReceived: Int = 0 var messagesReceived: Int = 0
var reconnectAttempts: Int = 0 var reconnectAttempts: Int = 0
@@ -347,7 +346,6 @@ final class NostrRelayManager: ObservableObject {
relays[index].nextReconnectTime = nil relays[index].nextReconnectTime = nil
if resetState { if resetState {
relays[index].lastError = nil relays[index].lastError = nil
relays[index].lastConnectedAt = nil
relays[index].lastDisconnectedAt = nil relays[index].lastDisconnectedAt = nil
relays[index].messagesSent = 0 relays[index].messagesSent = 0
relays[index].messagesReceived = 0 relays[index].messagesReceived = 0
@@ -1075,7 +1073,6 @@ final class NostrRelayManager: ObservableObject {
relays[index].isConnected = isConnected relays[index].isConnected = isConnected
relays[index].lastError = error relays[index].lastError = error
if isConnected { if isConnected {
relays[index].lastConnectedAt = dependencies.now()
relays[index].reconnectAttempts = 0 // Reset on successful connection relays[index].reconnectAttempts = 0 // Reset on successful connection
relays[index].nextReconnectTime = nil relays[index].nextReconnectTime = nil
} else { } else {
-8
View File
@@ -10,14 +10,6 @@ enum Geohash {
return map 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). /// Validates a geohash string at any channel precision (1-12 characters).
/// - Parameter geohash: The geohash string to validate /// - Parameter geohash: The geohash string to validate
/// - Returns: true if a non-empty base32 geohash of at most 12 characters /// - Returns: true if a non-empty base32 geohash of at most 12 characters
+1 -28
View File
@@ -11,14 +11,7 @@ import Foundation
/// Manages autocomplete functionality for chat /// Manages autocomplete functionality for chat
final class AutocompleteService { final class AutocompleteService {
private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: []) 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 /// Get autocomplete suggestions for current text
func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) { func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) {
let textToPosition = String(text.prefix(cursorPosition)) let textToPosition = String(text.prefix(cursorPosition))
@@ -73,26 +66,6 @@ final class AutocompleteService {
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange) 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 { private func needsArgument(command: String) -> Bool {
switch command { switch command {
case "/who", "/clear": case "/who", "/clear":
@@ -61,7 +61,6 @@ struct BLEFragmentAssemblyBuffer {
} }
private struct Metadata { private struct Metadata {
let type: UInt8
let total: Int let total: Int
let timestamp: Date let timestamp: Date
let isBroadcast: Bool let isBroadcast: Bool
@@ -150,7 +149,6 @@ struct BLEFragmentAssemblyBuffer {
fragmentsByKey[header.key] = [:] fragmentsByKey[header.key] = [:]
metadataByKey[header.key] = Metadata( metadataByKey[header.key] = Metadata(
type: header.originalType,
total: header.total, total: header.total,
timestamp: now, timestamp: now,
isBroadcast: header.isBroadcastFragment, 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 } guard let messageType = MessageType(rawValue: packet.type) else { return .low }
switch messageType { switch messageType {
case .fragment: 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) { mutating func markDisconnected(_ peerID: PeerID) {
guard var info = peers[peerID] else { return } guard var info = peers[peerID] else { return }
info.isConnected = false info.isConnected = false
+4 -14
View File
@@ -10,7 +10,6 @@ import UIKit
/// BLEService Bluetooth Mesh Transport /// BLEService Bluetooth Mesh Transport
/// - Emits events exclusively via `BitchatDelegate` for UI. /// - Emits events exclusively via `BitchatDelegate` for UI.
/// - ChatViewModel must consume delegate callbacks (`didReceivePublicMessage`, `didReceiveNoisePayload`). /// - ChatViewModel must consume delegate callbacks (`didReceivePublicMessage`, `didReceiveNoisePayload`).
/// - A lightweight `peerSnapshotPublisher` is provided for non-UI services.
final class BLEService: NSObject { final class BLEService: NSObject {
// MARK: - Constants // MARK: - Constants
@@ -493,13 +492,6 @@ final class BLEService: NSObject {
weak var delegate: BitchatDelegate? weak var delegate: BitchatDelegate?
weak var eventDelegate: TransportEventDelegate? weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate? 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] { func currentPeerSnapshots() -> [TransportPeerSnapshot] {
collectionsQueue.sync { collectionsQueue.sync {
@@ -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) { _ = collectionsQueue.sync(flags: .barrier) {
// Remove the peer when they leave // Remove the peer when they leave
peerRegistry.remove(peerID) 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) let senderID = PeerID(hexData: packet.senderID)
if packet.type != MessageType.announce.rawValue { if packet.type != MessageType.announce.rawValue {
@@ -3422,7 +3414,7 @@ extension BLEService {
/// Transport-level handling for a received nostrCarrier packet; policy /// Transport-level handling for a received nostrCarrier packet; policy
/// (verification of the carried event, quotas, loop prevention) lives in /// (verification of the carried event, quotas, loop prevention) lives in
/// `GatewayService` behind `onNostrCarrierPacket`. /// `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 senderID = PeerID(hexData: packet.senderID)
let directedToUs: Bool let directedToUs: Bool
if let recipientID = packet.recipientID { if let recipientID = packet.recipientID {
@@ -4476,7 +4468,7 @@ extension BLEService {
/// gossip backfill and hand the payload to the UI layer, where the group /// gossip backfill and hand the payload to the UI layer, where the group
/// coordinator decrypts and authenticates against the roster. Non-members /// coordinator decrypts and authenticates against the roster. Non-members
/// still relay (generic broadcast relay path) but never decode. /// 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 = { let isBroadcastRecipient: Bool = {
guard let recipient = packet.recipientID else { return true } guard let recipient = packet.recipientID else { return true }
return recipient.count == 8 && recipient.allSatisfy { $0 == 0xFF } return recipient.count == 8 && recipient.allSatisfy { $0 == 0xFF }
@@ -4636,8 +4628,6 @@ extension BLEService {
let transportPeers: [TransportPeerSnapshot] = collectionsQueue.sync { let transportPeers: [TransportPeerSnapshot] = collectionsQueue.sync {
peerRegistry.transportSnapshots(selfNickname: myNickname) peerRegistry.transportSnapshots(selfNickname: myNickname)
} }
// Notify non-UI listeners
peerSnapshotSubject.send(transportPeers)
// Notify UI on MainActor via delegate // Notify UI on MainActor via delegate
Task { @MainActor [weak self] in Task { @MainActor [weak self] in
self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers) self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers)
@@ -19,7 +19,6 @@ final class BoardManager: ObservableObject {
@Published private(set) var posts: [BoardPostPacket] = [] @Published private(set) var posts: [BoardPostPacket] = []
private let transport: Transport private let transport: Transport
private let store: BoardStore
/// Publishes a bridged kind-1 note (expiring with the board post via /// 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 /// NIP-40) and returns its Nostr event id, or nil when bridging failed or
/// was skipped. /// was skipped.
@@ -39,7 +38,6 @@ final class BoardManager: ObservableObject {
deleteFromNostr: ((String, String) -> Void)? = nil deleteFromNostr: ((String, String) -> Void)? = nil
) { ) {
self.transport = transport self.transport = transport
self.store = store
self.publishToNostr = publishToNostr ?? Self.livePublishToNostr self.publishToNostr = publishToNostr ?? Self.livePublishToNostr
self.deleteFromNostr = deleteFromNostr ?? Self.liveDeleteFromNostr self.deleteFromNostr = deleteFromNostr ?? Self.liveDeleteFromNostr
cancellable = store.$postsSnapshot cancellable = store.$postsSnapshot
-8
View File
@@ -146,14 +146,6 @@ final class BoardStore {
// MARK: - Maintenance // MARK: - Maintenance
func pruneExpired() {
let nowMs = currentMs()
queue.sync {
pruneExpiredLocked(nowMs: nowMs)
persistLocked()
}
}
/// Panic wipe: drop all board data from memory and disk. /// Panic wipe: drop all board data from memory and disk.
func wipe() { func wipe() {
queue.sync { queue.sync {
@@ -272,14 +272,6 @@ final class CourierStore {
// MARK: - Maintenance // MARK: - Maintenance
func pruneExpired() {
let date = now()
queue.sync {
pruneExpiredLocked(at: date)
persistLocked()
}
}
/// Panic wipe: drop all carried mail from memory and disk. /// Panic wipe: drop all carried mail from memory and disk.
func wipe() { func wipe() {
queue.sync { queue.sync {
@@ -58,12 +58,6 @@ final class StoreAndForwardMetrics {
SecureLogger.debug("📊 S&F \(event.rawValue)\(total)", category: .session) 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. /// Included in the panic wipe alongside the stores it describes.
func reset() { func reset() {
lock.lock() lock.lock()
@@ -82,7 +82,6 @@ final class GatewayService: ObservableObject {
let depositor: PeerID let depositor: PeerID
let geohash: String let geohash: String
let event: NostrEvent let event: NostrEvent
let queuedAt: Date
} }
static let shared = GatewayService() static let shared = GatewayService()
@@ -221,7 +220,7 @@ final class GatewayService: ObservableObject {
publish(event, geohash: carrier.geohash) publish(event, geohash: carrier.geohash)
accepted = true accepted = true
} else { } 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 // Only render on our own timeline what we actually accepted for
@@ -9,12 +9,12 @@
import Foundation import Foundation
/// Represents a participant in a geohash channel /// Represents a participant in a geohash channel
public struct GeoPerson: Identifiable, Equatable, Sendable { struct GeoPerson: Identifiable, Equatable, Sendable {
public let id: String // pubkey hex (lowercased) let id: String // pubkey hex (lowercased)
public let displayName: String let displayName: String
public let lastSeen: Date let lastSeen: Date
public init(id: String, displayName: String, lastSeen: Date) { init(id: String, displayName: String, lastSeen: Date) {
self.id = id self.id = id
self.displayName = displayName self.displayName = displayName
self.lastSeen = lastSeen self.lastSeen = lastSeen
@@ -23,7 +23,7 @@ public struct GeoPerson: Identifiable, Equatable, Sendable {
/// Protocol for resolving display names and checking block status /// Protocol for resolving display names and checking block status
@MainActor @MainActor
public protocol GeohashParticipantContext: AnyObject { protocol GeohashParticipantContext: AnyObject {
/// Returns display name for a Nostr pubkey (e.g., "alice#a1b2" or "anon#c3d4") /// Returns display name for a Nostr pubkey (e.g., "alice#a1b2" or "anon#c3d4")
func displayNameForPubkey(_ pubkeyHex: String) -> String func displayNameForPubkey(_ pubkeyHex: String) -> String
/// Returns true if the pubkey is blocked /// Returns true if the pubkey is blocked
@@ -32,16 +32,16 @@ public protocol GeohashParticipantContext: AnyObject {
/// Tracks participants across multiple geohash channels /// Tracks participants across multiple geohash channels
@MainActor @MainActor
public final class GeohashParticipantTracker: ObservableObject { final class GeohashParticipantTracker: ObservableObject {
/// Activity cutoff duration (defaults to 5 minutes) /// Activity cutoff duration (defaults to 5 minutes)
public let activityCutoff: TimeInterval let activityCutoff: TimeInterval
/// Per-geohash participant map: [geohash: [pubkeyHex: lastSeen]] /// Per-geohash participant map: [geohash: [pubkeyHex: lastSeen]]
private var participants: [String: [String: Date]] = [:] private var participants: [String: [String: Date]] = [:]
/// Currently visible people for the active geohash /// 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) /// The currently active geohash (if any)
private var activeGeohash: String? private var activeGeohash: String?
@@ -52,17 +52,17 @@ public final class GeohashParticipantTracker: ObservableObject {
/// Timer for periodic refresh /// Timer for periodic refresh
private var refreshTimer: Timer? private var refreshTimer: Timer?
public init(activityCutoff: TimeInterval = -300) { // default 5 minutes init(activityCutoff: TimeInterval = -300) { // default 5 minutes
self.activityCutoff = activityCutoff self.activityCutoff = activityCutoff
} }
/// Configure with a context provider /// Configure with a context provider
public func configure(context: GeohashParticipantContext) { func configure(context: GeohashParticipantContext) {
self.context = context self.context = context
} }
/// Set the currently active geohash /// Set the currently active geohash
public func setActiveGeohash(_ geohash: String?) { func setActiveGeohash(_ geohash: String?) {
activeGeohash = geohash activeGeohash = geohash
if geohash == nil { if geohash == nil {
visiblePeople = [] visiblePeople = []
@@ -72,13 +72,13 @@ public final class GeohashParticipantTracker: ObservableObject {
} }
/// Record activity from a participant in the current active geohash /// 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 } guard let gh = activeGeohash else { return }
recordParticipant(pubkeyHex: pubkeyHex, geohash: gh) recordParticipant(pubkeyHex: pubkeyHex, geohash: gh)
} }
/// Record activity from a participant in a specific geohash /// 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() let key = pubkeyHex.lowercased()
var map = participants[geohash] ?? [:] var map = participants[geohash] ?? [:]
map[key] = Date() map[key] = Date()
@@ -94,7 +94,7 @@ public final class GeohashParticipantTracker: ObservableObject {
} }
/// Remove a participant from all geohashes (used when blocking) /// Remove a participant from all geohashes (used when blocking)
public func removeParticipant(pubkeyHex: String) { func removeParticipant(pubkeyHex: String) {
let key = pubkeyHex.lowercased() let key = pubkeyHex.lowercased()
for (gh, var map) in participants { for (gh, var map) in participants {
map.removeValue(forKey: key) map.removeValue(forKey: key)
@@ -104,14 +104,14 @@ public final class GeohashParticipantTracker: ObservableObject {
} }
/// Get participant count for a specific geohash /// 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 cutoff = Date().addingTimeInterval(activityCutoff)
let map = participants[geohash] ?? [:] let map = participants[geohash] ?? [:]
return map.values.filter { $0 >= cutoff }.count return map.values.filter { $0 >= cutoff }.count
} }
/// Get the visible people list for the active geohash (read-only query) /// 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 [] } guard let gh = activeGeohash, let context = context else { return [] }
let cutoff = Date().addingTimeInterval(activityCutoff) let cutoff = Date().addingTimeInterval(activityCutoff)
let map = (participants[gh] ?? [:]) let map = (participants[gh] ?? [:])
@@ -126,12 +126,12 @@ public final class GeohashParticipantTracker: ObservableObject {
} }
/// Refresh the visible people list /// Refresh the visible people list
public func refresh() { func refresh() {
visiblePeople = getVisiblePeople() visiblePeople = getVisiblePeople()
} }
/// Start the periodic refresh timer /// Start the periodic refresh timer
public func startRefreshTimer(interval: TimeInterval = 30.0) { func startRefreshTimer(interval: TimeInterval = 30.0) {
stopRefreshTimer() stopRefreshTimer()
refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
Task { @MainActor in Task { @MainActor in
@@ -141,19 +141,19 @@ public final class GeohashParticipantTracker: ObservableObject {
} }
/// Stop the periodic refresh timer /// Stop the periodic refresh timer
public func stopRefreshTimer() { func stopRefreshTimer() {
refreshTimer?.invalidate() refreshTimer?.invalidate()
refreshTimer = nil refreshTimer = nil
} }
/// Clear all participant data /// Clear all participant data
public func clear() { func clear() {
participants.removeAll() participants.removeAll()
visiblePeople = [] visiblePeople = []
} }
/// Clear participant data for a specific geohash /// Clear participant data for a specific geohash
public func clear(geohash: String) { func clear(geohash: String) {
participants.removeValue(forKey: geohash) participants.removeValue(forKey: geohash)
if activeGeohash == geohash { if activeGeohash == geohash {
visiblePeople = [] visiblePeople = []
@@ -406,7 +406,6 @@ enum GroupCryptoError: Error, Equatable {
case malformedPayload case malformedPayload
case signingFailed case signingFailed
case sealFailed case sealFailed
case wrongEpoch
case decryptionFailed case decryptionFailed
case badSenderSignature case badSenderSignature
} }
+1 -26
View File
@@ -255,11 +255,6 @@ final class KeychainManager: KeychainManagerProtocol {
// MARK: - Generic Operations // 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 { private func saveData(_ data: Data, forKey key: String) -> Bool {
// Delete any existing item first to ensure clean state // Delete any existing item first to ensure clean state
_ = delete(forKey: key) _ = delete(forKey: key)
@@ -305,11 +300,6 @@ final class KeychainManager: KeychainManagerProtocol {
return false 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? { private func retrieveData(forKey key: String) -> Data? {
// Base query // Base query
let base: [String: Any] = [ let base: [String: Any] = [
@@ -365,22 +355,7 @@ final class KeychainManager: KeychainManagerProtocol {
} }
// MARK: - Cleanup // 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 // Delete ALL keychain data for panic mode
func deleteAllKeychainData() -> Bool { func deleteAllKeychainData() -> Bool {
SecureLogger.warning("Panic mode - deleting all keychain data", category: .security) 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 cl: LocationStateManaging
private let geocoder: LocationStateGeocoding private let geocoder: LocationStateGeocoding
private var lastLocation: CLLocation?
private var refreshTimer: Timer? private var refreshTimer: Timer?
private var isGeocoding: Bool = false
// MARK: - Persistence Keys // 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 } guard permissionState == .authorized else { return }
refreshTimer?.invalidate() refreshTimer?.invalidate()
refreshTimer = nil refreshTimer = nil
@@ -385,7 +383,6 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let loc = locations.last else { return } guard let loc = locations.last else { return }
lastLocation = loc
computeChannels(from: loc.coordinate) computeChannels(from: loc.coordinate)
reverseGeocodeLocation(loc) reverseGeocodeLocation(loc)
} }
@@ -441,10 +438,8 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
private func reverseGeocodeLocation(_ location: CLLocation) { private func reverseGeocodeLocation(_ location: CLLocation) {
geocoder.cancelGeocode() geocoder.cancelGeocode()
isGeocoding = true
geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, _ in geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, _ in
guard let self = self else { return } guard let self = self else { return }
self.isGeocoding = false
if let pm = placemarks?.first { if let pm = placemarks?.first {
let names = self.locationNamesByLevel(from: pm) let names = self.locationNamesByLevel(from: pm)
Task { @MainActor in self.locationNames = names } Task { @MainActor in self.locationNames = names }
@@ -632,15 +627,5 @@ extension LocationStateManager {
func toggle(_ geohash: String) { func toggle(_ geohash: String) {
toggleBookmark(geohash) 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 #endif
@@ -70,10 +70,6 @@ final class MessageFormattingEngine {
static let quickCashuPresence: NSRegularExpression = { static let quickCashuPresence: NSRegularExpression = {
try! NSRegularExpression(pattern: "\\bcashu[AB][A-Za-z0-9._-]{40,}\\b", options: []) 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 // MARK: - Match Types
@@ -195,7 +191,7 @@ final class MessageFormattingEngine {
// MARK: - Private Helpers // MARK: - Private Helpers
private static func formatSystemMessage(_ message: BitchatMessage, isDark: Bool) -> AttributedString { private static func formatSystemMessage(_ message: BitchatMessage, isDark _: Bool) -> AttributedString {
var result = AttributedString() var result = AttributedString()
let content = AttributedString("* \(message.content) *") let content = AttributedString("* \(message.content) *")
@@ -414,7 +410,7 @@ final class MessageFormattingEngine {
return AttributedString(text).mergingAttributes(style) 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() var style = AttributeContainer()
switch type { 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) { func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
if let transport = connectedTransport(for: peerID) { if let transport = connectedTransport(for: peerID) {
transport.sendFavoriteNotification(to: peerID, isFavorite: isFavorite) transport.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
@@ -153,11 +153,11 @@ enum EncryptionStatus: Equatable {
final class NoiseEncryptionService { final class NoiseEncryptionService {
// Static identity key (persistent across sessions) // Static identity key (persistent across sessions)
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
// Ed25519 signing key (persistent across sessions) // Ed25519 signing key (persistent across sessions)
private let signingKey: Curve25519.Signing.PrivateKey private let signingKey: Curve25519.Signing.PrivateKey
public let signingPublicKey: Curve25519.Signing.PublicKey let signingPublicKey: Curve25519.Signing.PublicKey
// Session manager // Session manager
private let sessionManager: NoiseSessionManager private let sessionManager: NoiseSessionManager
+1 -10
View File
@@ -13,7 +13,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
let currentIdentity: @MainActor () throws -> NostrIdentity? let currentIdentity: @MainActor () throws -> NostrIdentity?
let registerPendingGiftWrap: @MainActor (String) -> Void let registerPendingGiftWrap: @MainActor (String) -> Void
let sendEvent: @MainActor (NostrEvent) -> Void let sendEvent: @MainActor (NostrEvent) -> Void
let scheduleAfter: @Sendable (TimeInterval, @escaping @Sendable () -> Void) -> Void
/// Emits whether a relay that carries private messages is up /// Emits whether a relay that carries private messages is up
/// (fail-closed behind Tor). A connected geohash/custom relay alone /// (fail-closed behind Tor). A connected geohash/custom relay alone
/// doesn't count: DM sends target the default relay set and would /// 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.currentIdentity = currentIdentity
self.registerPendingGiftWrap = registerPendingGiftWrap self.registerPendingGiftWrap = registerPendingGiftWrap
self.sendEvent = sendEvent self.sendEvent = sendEvent
self.scheduleAfter = scheduleAfter
self.relayConnectivity = relayConnectivity self.relayConnectivity = relayConnectivity
// Default pacer drives its throttle through the same injected // Default pacer drives its throttle through the same injected
// scheduler, so tests that step scheduleAfter manually keep // scheduler, so tests that step scheduleAfter manually keep
@@ -130,8 +128,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
} }
} }
static let sharedAckPacer = AckPacer() static let sharedAckPacer = AckPacer()
private let keychain: KeychainManagerProtocol
private let idBridge: NostrIdentityBridge
private let dependencies: Dependencies private let dependencies: Dependencies
private var favoriteStatusObserver: NSObjectProtocol? private var favoriteStatusObserver: NSObjectProtocol?
@@ -145,12 +141,10 @@ final class NostrTransport: Transport, @unchecked Sendable {
@MainActor @MainActor
init( init(
keychain: KeychainManagerProtocol, keychain _: KeychainManagerProtocol,
idBridge: NostrIdentityBridge, idBridge: NostrIdentityBridge,
dependencies: Dependencies? = nil dependencies: Dependencies? = nil
) { ) {
self.keychain = keychain
self.idBridge = idBridge
self.dependencies = dependencies ?? .live(idBridge: idBridge) self.dependencies = dependencies ?? .live(idBridge: idBridge)
setupObservers() setupObservers()
@@ -207,9 +201,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
weak var eventDelegate: TransportEventDelegate? weak var eventDelegate: TransportEventDelegate?
weak var peerEventsDelegate: TransportPeerEventsDelegate? weak var peerEventsDelegate: TransportPeerEventsDelegate?
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
Just([]).eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] } func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] }
var myPeerID: PeerID { senderPeerID } var myPeerID: PeerID { senderPeerID }
+1 -14
View File
@@ -28,7 +28,6 @@ final class PrivateChatManager: ObservableObject {
@Published private(set) var selectedPeer: PeerID? = nil @Published private(set) var selectedPeer: PeerID? = nil
private var selectedPeerMirrorCancellable: AnyCancellable? = nil private var selectedPeerMirrorCancellable: AnyCancellable? = nil
private var selectedPeerFingerprint: String? = nil
var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel
weak var meshService: Transport? weak var meshService: Transport?
@@ -219,18 +218,13 @@ final class PrivateChatManager: ObservableObject {
/// Start a private chat with a peer. Selection is mutated through the /// Start a private chat with a peer. Selection is mutated through the
/// store's intent (the store owns it); the manager keeps its side /// 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 @MainActor
func startChat(with peerID: PeerID) { func startChat(with peerID: PeerID) {
// Also creates the conversation if needed and updates the derived // Also creates the conversation if needed and updates the derived
// `selectedConversationID`; `selectedPeer` mirrors the change. // `selectedConversationID`; `selectedPeer` mirrors the change.
conversationStore?.setSelectedPrivatePeer(peerID) conversationStore?.setSelectedPrivatePeer(peerID)
// Store fingerprint for persistence across reconnections
if let fingerprint = meshService?.getFingerprint(for: peerID) {
selectedPeerFingerprint = fingerprint
}
// Mark messages as read // Mark messages as read
markAsRead(from: peerID) markAsRead(from: peerID)
} }
@@ -239,15 +233,8 @@ final class PrivateChatManager: ObservableObject {
/// channel's conversation). /// channel's conversation).
func endChat() { func endChat() {
conversationStore?.setSelectedPrivatePeer(nil) 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 /// Mark messages from a peer as read
@MainActor @MainActor
func markAsRead(from peerID: PeerID) { func markAsRead(from peerID: PeerID) {
+1 -1
View File
@@ -12,7 +12,7 @@ struct RelayController {
static func decide(ttl: UInt8, static func decide(ttl: UInt8,
senderIsSelf: Bool, senderIsSelf: Bool,
recipientIsSelf: Bool = false, recipientIsSelf: Bool = false,
isEncrypted: Bool, isEncrypted _: Bool,
isDirectedEncrypted: Bool, isDirectedEncrypted: Bool,
isFragment: Bool, isFragment: Bool,
isDirectedFragment: 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)? { func snapshot(id: String) -> (sent: Int, total: Int)? {
var result: (sent: Int, total: Int)? var result: (sent: Int, total: Int)?
queue.sync { queue.sync {
+1 -2
View File
@@ -96,7 +96,6 @@ protocol Transport: AnyObject {
var peerEventsDelegate: TransportPeerEventsDelegate? { get set } var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
// Peer snapshots (for non-UI services) // Peer snapshots (for non-UI services)
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
func currentPeerSnapshots() -> [TransportPeerSnapshot] func currentPeerSnapshots() -> [TransportPeerSnapshot]
// Identity // Identity
@@ -265,7 +264,7 @@ extension Transport {
} }
protocol TransportPeerEventsDelegate: AnyObject { protocol TransportPeerEventsDelegate: AnyObject {
@MainActor func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot]) @MainActor func didUpdatePeerSnapshots(_: [TransportPeerSnapshot])
} }
extension BitchatDelegate { extension BitchatDelegate {
+2 -17
View File
@@ -27,7 +27,6 @@ enum TransportConfig {
static let pttJitterBufferSeconds: TimeInterval = 0.35 // buffered audio before live playback starts 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 pttJitterDeadlineSeconds: TimeInterval = 0.5 // start anyway after this wall-clock wait
static let pttBurstEndTimeoutSeconds: TimeInterval = 3.0 // no frames -> burst considered ended 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 pttMaxConcurrentAssemblies: Int = 8 // concurrent inbound bursts cap
static let pttMaxBurstBytes: Int = 384 * 1024 // 120s at ~2KB/s + generous slack static let pttMaxBurstBytes: Int = 384 * 1024 // 120s at ~2KB/s + generous slack
static let pttFinishedBurstRegistrySeconds: TimeInterval = 600 // window to absorb the finalized note static let pttFinishedBurstRegistrySeconds: TimeInterval = 600 // window to absorb the finalized note
@@ -93,8 +92,7 @@ enum TransportConfig {
// UI thresholds // UI thresholds
static let uiProcessedNostrEventsCap: Int = 2000 static let uiProcessedNostrEventsCap: Int = 2000
static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60
// UI rate limiters (token buckets) // UI rate limiters (token buckets)
static let uiSenderRateBucketCapacity: Double = 5 static let uiSenderRateBucketCapacity: Double = 5
static let uiSenderRateBucketRefillPerSec: Double = 1.0 static let uiSenderRateBucketRefillPerSec: Double = 1.0
@@ -103,17 +101,13 @@ enum TransportConfig {
// UI sleeps/delays // UI sleeps/delays
static let uiStartupInitialDelaySeconds: TimeInterval = 1.0 static let uiStartupInitialDelaySeconds: TimeInterval = 1.0
static let uiStartupShortSleepNs: UInt64 = 200_000_000
static let uiStartupPhaseDurationSeconds: TimeInterval = 2.0 static let uiStartupPhaseDurationSeconds: TimeInterval = 2.0
static let uiAsyncShortSleepNs: UInt64 = 100_000_000 static let uiAsyncShortSleepNs: UInt64 = 100_000_000
static let uiAsyncMediumSleepNs: UInt64 = 500_000_000
static let uiReadReceiptRetryShortSeconds: TimeInterval = 0.1 static let uiReadReceiptRetryShortSeconds: TimeInterval = 0.1
static let uiReadReceiptRetryLongSeconds: TimeInterval = 0.5 static let uiReadReceiptRetryLongSeconds: TimeInterval = 0.5
static let uiBatchDispatchStaggerSeconds: TimeInterval = 0.15 static let uiBatchDispatchStaggerSeconds: TimeInterval = 0.15
static let uiScrollThrottleSeconds: TimeInterval = 0.5 static let uiScrollThrottleSeconds: TimeInterval = 0.5
static let uiAnimationShortSeconds: TimeInterval = 0.15
static let uiAnimationMediumSeconds: TimeInterval = 0.2 static let uiAnimationMediumSeconds: TimeInterval = 0.2
static let uiAnimationSidebarSeconds: TimeInterval = 0.25
static let uiRecentCutoffFiveMinutesSeconds: TimeInterval = 5 * 60 static let uiRecentCutoffFiveMinutesSeconds: TimeInterval = 5 * 60
static let uiMeshEmptyConfirmationSeconds: TimeInterval = 30.0 static let uiMeshEmptyConfirmationSeconds: TimeInterval = 30.0
@@ -179,10 +173,6 @@ enum TransportConfig {
static let nostrGeohashSampleLimit: Int = 100 static let nostrGeohashSampleLimit: Int = 100
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400 static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
// Nostr helpers
static let nostrShortKeyDisplayLength: Int = 8
static let nostrConvKeyPrefixLength: Int = 16
// Message deduplication // Message deduplication
static let messageDedupMaxAgeSeconds: TimeInterval = 300 static let messageDedupMaxAgeSeconds: TimeInterval = 300
static let messageDedupMaxCount: Int = 1000 static let messageDedupMaxCount: Int = 1000
@@ -294,12 +284,7 @@ enum TransportConfig {
static let uiVeryLongTokenThreshold: Int = 512 static let uiVeryLongTokenThreshold: Int = 512
static let uiLongMessageLineLimit: Int = 30 static let uiLongMessageLineLimit: Int = 30
static let uiFingerprintSampleCount: Int = 3 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 // UI color tuning
static let uiColorHueAvoidanceDelta: Double = 0.05 static let uiColorHueAvoidanceDelta: Double = 0.05
static let uiColorHueOffset: Double = 0.12 static let uiColorHueOffset: Double = 0.12
+2 -7
View File
@@ -70,7 +70,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
} }
// TransportPeerEventsDelegate // TransportPeerEventsDelegate
func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot]) { func didUpdatePeerSnapshots(_: [TransportPeerSnapshot]) {
updatePeers() updatePeers()
} }
@@ -367,12 +367,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
} }
// MARK: - Compatibility Methods (for easy migration) // 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> { var blockedUsers: Set<String> {
Set(peers.compactMap { peer in Set(peers.compactMap { peer in
isBlocked(peer.peerID) ? getFingerprint(for: peer.peerID) : nil isBlocked(peer.peerID) ? getFingerprint(for: peer.peerID) : nil
-19
View File
@@ -52,18 +52,6 @@ final class MessageDeduplicator {
return false 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) /// Add an ID without checking (for announce-back tracking)
func markProcessed(_ id: String) { func markProcessed(_ id: String) {
lock.lock() lock.lock()
@@ -83,13 +71,6 @@ final class MessageDeduplicator {
return lookup[id] != nil 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() { private func trimIfNeeded() {
let activeCount = entries.count - head let activeCount = entries.count - head
guard activeCount > maxCount else { return } 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( ThemePalette(
background: systemBackground, background: systemBackground,
primary: .primary, primary: .primary,
@@ -387,7 +387,7 @@ final class ChatGroupCoordinator {
/// Decrypt-verify path for an incoming 0x25 broadcast. Drops silently for /// Decrypt-verify path for an incoming 0x25 broadcast. Drops silently for
/// unknown groups (non-members relay but never read), wrong epochs, bad /// unknown groups (non-members relay but never read), wrong epochs, bad
/// sender signatures, and senders missing from the pinned roster. /// 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 envelope = GroupMessageEnvelope.decode(payload) else { return }
guard let group = context.groupStore.group(withID: envelope.groupID) else { return } guard let group = context.groupStore.group(withID: envelope.groupID) else { return }
guard envelope.epoch == group.epoch else { guard envelope.epoch == group.epoch else {
@@ -29,7 +29,6 @@ protocol ChatPeerIdentityContext: AnyObject {
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID) func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
var selectedPrivateChatPeer: PeerID? { get set } var selectedPrivateChatPeer: PeerID? { get set }
var selectedPrivateChatFingerprint: String? { get set } var selectedPrivateChatFingerprint: String? { get set }
var nickname: String { get }
var myPeerID: PeerID { get } var myPeerID: PeerID { get }
var activeChannel: ChannelID { get } var activeChannel: ChannelID { get }
/// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`). /// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`).
@@ -104,7 +103,7 @@ protocol ChatPeerIdentityContext: AnyObject {
extension ChatViewModel: ChatPeerIdentityContext { extension ChatViewModel: ChatPeerIdentityContext {
// `privateChats`, `unreadPrivateMessages`, `selectedPrivateChatPeer`, // `privateChats`, `unreadPrivateMessages`, `selectedPrivateChatPeer`,
// `selectedPrivateChatFingerprint`, `nickname`, `myPeerID`, // `selectedPrivateChatFingerprint`, `myPeerID`,
// `activeChannel`, `connectedPeers`, `geoNicknames`, `notifyUIChanged()`, // `activeChannel`, `connectedPeers`, `geoNicknames`, `notifyUIChanged()`,
// `addSystemMessage(_:)`, `peerNickname(for:)`, `meshPeerNicknames()`, // `addSystemMessage(_:)`, `peerNickname(for:)`, `meshPeerNicknames()`,
// `ephemeralPeerID(forNoiseKey:)`, `unifiedPeer(for:)`, // `ephemeralPeerID(forNoiseKey:)`, `unifiedPeer(for:)`,
@@ -19,7 +19,6 @@ protocol ChatPrivateConversationContext: AnyObject {
/// lookup on `ChatViewModel` (no `privateChats` dictionary build). /// lookup on `ChatViewModel` (no `privateChats` dictionary build).
func privateMessages(for peerID: PeerID) -> [BitchatMessage] func privateMessages(for peerID: PeerID) -> [BitchatMessage]
var sentReadReceipts: Set<String> { get } var sentReadReceipts: Set<String> { get }
var unreadPrivateMessages: Set<PeerID> { get }
var selectedPrivateChatPeer: PeerID? { get } var selectedPrivateChatPeer: PeerID? { get }
var nickname: String { get } var nickname: String { get }
var activeChannel: ChannelID { get } var activeChannel: ChannelID { get }
@@ -40,8 +39,6 @@ protocol ChatPrivateConversationContext: AnyObject {
func setPrivateDeliveryStatus(_ status: DeliveryStatus, forMessageID messageID: String, peerID: PeerID) -> Bool func setPrivateDeliveryStatus(_ status: DeliveryStatus, forMessageID messageID: String, peerID: PeerID) -> Bool
func markPrivateChatUnread(_ peerID: PeerID) func markPrivateChatUnread(_ peerID: PeerID)
func markPrivateChatRead(_ 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 /// Moves all messages from `oldPeerID`'s chat into `newPeerID`'s chat
/// (dedup by ID, order preserved, unread carried, old chat removed). /// (dedup by ID, order preserved, unread carried, old chat removed).
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID) func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
@@ -87,7 +84,6 @@ protocol ChatPrivateConversationContext: AnyObject {
// MARK: Routing & acknowledgements // MARK: Routing & acknowledgements
func routePrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) func routePrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String)
func routeReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) func routeReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
func sendGeohashPrivateMessage(_ content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String) func sendGeohashPrivateMessage(_ content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String)
func sendGeohashDeliveryAck(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity) 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 /// O(1)-per-conversation dedup via the store's message-ID indexes
/// (replaces the full scan over every private chat). /// (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) 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 { func isMessageBlocked(_ message: BitchatMessage) -> Bool {
if let peerID = message.senderPeerID ?? context.getPeerIDForNickname(message.sender) { if let peerID = message.senderPeerID ?? context.getPeerIDForNickname(message.sender) {
if context.isPeerBlocked(peerID) { return true } if context.isPeerBlocked(peerID) { return true }
@@ -36,9 +36,6 @@ protocol ChatPublicConversationContext: AnyObject {
/// message with the same ID is already in that conversation. /// message with the same ID is already in that conversation.
@discardableResult @discardableResult
func appendPublicMessage(_ message: BitchatMessage, to conversationID: ConversationID) -> Bool 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 func publicConversationContainsMessage(withID messageID: String, in conversationID: ConversationID) -> Bool
/// Removes a message by ID from whichever public conversation contains it. /// Removes a message by ID from whichever public conversation contains it.
@discardableResult @discardableResult
@@ -109,7 +106,6 @@ extension ChatViewModel: ChatPublicConversationContext {
// `geoParticipantCount(for:)`, `isNostrBlocked(pubkeyHexLowercased:)`, // `geoParticipantCount(for:)`, `isNostrBlocked(pubkeyHexLowercased:)`,
// `deriveNostrIdentity(forGeohash:)`, the public conversation store // `deriveNostrIdentity(forGeohash:)`, the public conversation store
// intents (`appendPublicMessage(_:to:)`, // intents (`appendPublicMessage(_:to:)`,
// `appendGeohashMessageIfAbsent(_:toGeohash:)`,
// `publicConversationContainsMessage(withID:in:)`, // `publicConversationContainsMessage(withID:in:)`,
// `removePublicMessage(withID:)`, // `removePublicMessage(withID:)`,
// `removePublicMessages(fromGeohash:where:)`, // `removePublicMessages(fromGeohash:where:)`,
@@ -520,27 +516,27 @@ final class ChatPublicConversationCoordinator: PublicMessagePipelineDelegate {
#endif #endif
} }
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String { func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String {
context.normalizedContentKey(content) context.normalizedContentKey(content)
} }
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? { func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
context.contentTimestamp(forKey: key) 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) 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) context.appendPublicMessage(message, to: conversationID)
} }
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage) { func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage) {
context.prewarmMessageFormatting(message) context.prewarmMessageFormatting(message)
} }
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) { func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool) {
context.setPublicBatching(isBatching) context.setPublicBatching(isBatching)
} }
} }
@@ -135,7 +135,6 @@ final class ChatVerificationCoordinator {
let noiseKeyHex: String let noiseKeyHex: String
let signKeyHex: String let signKeyHex: String
let nonceA: Data let nonceA: Data
let startedAt: Date
var sent: Bool var sent: Bool
} }
@@ -258,7 +257,6 @@ final class ChatVerificationCoordinator {
noiseKeyHex: qr.noiseKeyHex, noiseKeyHex: qr.noiseKeyHex,
signKeyHex: qr.signKeyHex, signKeyHex: qr.signKeyHex,
nonceA: nonce, nonceA: nonce,
startedAt: Date(),
sent: false sent: false
) )
pendingQRVerifications[peerID] = pending pendingQRVerifications[peerID] = pending
+1 -51
View File
@@ -84,7 +84,6 @@ import SwiftUI
import Combine import Combine
import CommonCrypto import CommonCrypto
import CoreBluetooth import CoreBluetooth
import Tor
#if os(iOS) #if os(iOS)
import UIKit import UIKit
#endif #endif
@@ -222,12 +221,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
conversations.unreadDirectRoutingPeerIDs() 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. /// Open the most relevant private chat when tapping the toolbar unread icon.
/// Prefers the most recently active unread conversation, otherwise the most recent PM. /// Prefers the most recently active unread conversation, otherwise the most recent PM.
@MainActor @MainActor
@@ -245,20 +238,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
set { peerIdentityStore.setSelectedPrivateChatFingerprint(newValue) } 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) // Resolve short mesh ID (16-hex) from a full Noise public key hex (64-hex)
@MainActor @MainActor
func getShortIDForNoiseKey(_ fullNoiseKeyHex: PeerID) -> PeerID { func getShortIDForNoiseKey(_ fullNoiseKeyHex: PeerID) -> PeerID {
@@ -354,18 +333,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// MARK: - Social Features (Delegated to PeerStateManager) // MARK: - Social Features (Delegated to PeerStateManager)
@MainActor
var favoritePeers: Set<String> { unifiedPeerService.favoritePeers }
@MainActor @MainActor
var blockedUsers: Set<String> { unifiedPeerService.blockedUsers } var blockedUsers: Set<String> { unifiedPeerService.blockedUsers }
// MARK: - Encryption and Security // MARK: - Encryption and Security
// Noise Protocol encryption status
var peerEncryptionStatus: [PeerID: EncryptionStatus] {
get { peerIdentityStore.encryptionStatuses }
set { peerIdentityStore.replaceEncryptionStatuses(newValue) }
}
var verifiedFingerprints: Set<String> { var verifiedFingerprints: Set<String> {
get { peerIdentityStore.verifiedFingerprints } get { peerIdentityStore.verifiedFingerprints }
set { peerIdentityStore.setVerifiedFingerprints(newValue) } set { peerIdentityStore.setVerifiedFingerprints(newValue) }
@@ -959,11 +931,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
// MARK: - Public helpers // 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. /// Return the current, pruned, sorted people list for the active geohash without mutating state.
@MainActor @MainActor
func visibleGeohashPeople() -> [GeoPerson] { func visibleGeohashPeople() -> [GeoPerson] {
@@ -1056,14 +1023,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
publicConversationCoordinator.displayNameForNostrPubkey(pubkeyHex) publicConversationCoordinator.displayNameForNostrPubkey(pubkeyHex)
} }
// MARK: - Media Transfers
private enum MediaSendError: Error {
case encodingFailed
case tooLarge
case copyFailed
}
func currentPublicSender() -> (name: String, peerID: PeerID) { func currentPublicSender() -> (name: String, peerID: PeerID) {
publicConversationCoordinator.currentPublicSender() publicConversationCoordinator.currentPublicSender()
} }
@@ -1125,7 +1084,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
} }
@MainActor @MainActor
@objc func handlePeerStatusUpdate(_ notification: Notification) { @objc func handlePeerStatusUpdate(_: Notification) {
peerIdentityCoordinator.handlePeerStatusUpdate() peerIdentityCoordinator.handlePeerStatusUpdate()
} }
@@ -1159,15 +1118,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
lifecycleCoordinator.markPrivateMessagesAsRead(from: peerID) 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 @MainActor
func getPeerIDForNickname(_ nickname: String) -> PeerID? { func getPeerIDForNickname(_ nickname: String) -> PeerID? {
peerIdentityCoordinator.getPeerIDForNickname(nickname) peerIdentityCoordinator.getPeerIDForNickname(nickname)
@@ -35,11 +35,6 @@ extension ChatViewModel {
nostrCoordinator.inbound.handleNostrEvent(event) nostrCoordinator.inbound.handleNostrEvent(event)
} }
@MainActor
func subscribeToGeoChat(_ ch: GeohashChannel) {
nostrCoordinator.subscriptions.subscribeToGeoChat(ch)
}
@MainActor @MainActor
func handleGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) { func handleGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) {
nostrCoordinator.inbound.handleGiftWrap(giftWrap, id: id) nostrCoordinator.inbound.handleGiftWrap(giftWrap, id: id)
@@ -55,21 +50,11 @@ extension ChatViewModel {
nostrCoordinator.subscriptions.beginGeohashSampling(for: geohashes) nostrCoordinator.subscriptions.beginGeohashSampling(for: geohashes)
} }
@MainActor
func subscribe(_ gh: String) {
nostrCoordinator.subscriptions.subscribe(gh)
}
@MainActor @MainActor
func subscribeNostrEvent(_ event: NostrEvent, gh: String) { func subscribeNostrEvent(_ event: NostrEvent, gh: String) {
nostrCoordinator.presence.subscribeNostrEvent(event, gh: gh) nostrCoordinator.presence.subscribeNostrEvent(event, gh: gh)
} }
@MainActor
func cooldownPerGeohash(_ gh: String, content: String, event: NostrEvent) {
nostrCoordinator.presence.cooldownPerGeohash(gh, content: content, event: event)
}
@MainActor @MainActor
func endGeohashSampling() { func endGeohashSampling() {
nostrCoordinator.subscriptions.endGeohashSampling() nostrCoordinator.subscriptions.endGeohashSampling()
@@ -80,35 +65,11 @@ extension ChatViewModel {
nostrCoordinator.subscriptions.setupNostrMessageHandling() nostrCoordinator.subscriptions.setupNostrMessageHandling()
} }
@MainActor
func handleNostrMessage(_ giftWrap: NostrEvent) {
nostrCoordinator.inbound.handleNostrMessage(giftWrap)
}
func processNostrMessage(_ giftWrap: NostrEvent) async {
await nostrCoordinator.inbound.processNostrMessage(giftWrap)
}
@MainActor @MainActor
func findNoiseKey(for nostrPubkey: String) -> Data? { func findNoiseKey(for nostrPubkey: String) -> Data? {
nostrCoordinator.inbound.findNoiseKey(for: nostrPubkey) 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 @MainActor
func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) { func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
nostrCoordinator.sendFavoriteNotificationViaNostr(noisePublicKey: noisePublicKey, isFavorite: isFavorite) nostrCoordinator.sendFavoriteNotificationViaNostr(noisePublicKey: noisePublicKey, isFavorite: isFavorite)
@@ -55,16 +55,6 @@ extension ChatViewModel {
privateConversationCoordinator.handleReadReceipt(payload, senderPubkey: senderPubkey, convKey: convKey) 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 @MainActor
func sendVoiceNote(at url: URL) { func sendVoiceNote(at url: URL) {
mediaTransferCoordinator.sendVoiceNote(at: url) mediaTransferCoordinator.sendVoiceNote(at: url)
@@ -162,11 +152,6 @@ extension ChatViewModel {
mediaTransferCoordinator.clearTransferMapping(for: messageID) mediaTransferCoordinator.clearTransferMapping(for: messageID)
} }
@MainActor
func handleMediaSendFailure(messageID: String, reason: String) {
mediaTransferCoordinator.handleMediaSendFailure(messageID: messageID, reason: reason)
}
@MainActor @MainActor
func handleTransferEvent(_ event: TransferProgressManager.Event) { func handleTransferEvent(_ event: TransferProgressManager.Event) {
mediaTransferCoordinator.handleTransferEvent(event) mediaTransferCoordinator.handleTransferEvent(event)
@@ -194,59 +179,6 @@ extension ChatViewModel {
privateConversationCoordinator.handlePrivateMessage(message) 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 @MainActor
func processActionMessage(_ message: BitchatMessage) -> BitchatMessage { func processActionMessage(_ message: BitchatMessage) -> BitchatMessage {
privateConversationCoordinator.processActionMessage(message) privateConversationCoordinator.processActionMessage(message)
@@ -257,11 +189,6 @@ extension ChatViewModel {
privateConversationCoordinator.migratePrivateChatsIfNeeded(for: peerID, senderNickname: senderNickname) privateConversationCoordinator.migratePrivateChatsIfNeeded(for: peerID, senderNickname: senderNickname)
} }
@MainActor
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
privateConversationCoordinator.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
}
@MainActor @MainActor
func isMessageBlocked(_ message: BitchatMessage) -> Bool { func isMessageBlocked(_ message: BitchatMessage) -> Bool {
privateConversationCoordinator.isMessageBlocked(message) privateConversationCoordinator.isMessageBlocked(message)
@@ -54,7 +54,7 @@ extension ChatViewModel {
} }
} }
@objc func handleTorPreferenceChanged(_ notification: Notification) { @objc func handleTorPreferenceChanged(_: Notification) {
Task { @MainActor in Task { @MainActor in
self.torStatusAnnounced = false self.torStatusAnnounced = false
self.torInitialReadyAnnounced = false self.torInitialReadyAnnounced = false
@@ -79,9 +79,4 @@ struct MessageRateLimiter {
return senderAllowed && contentAllowed 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) return Color(hue: entry.hue, saturation: saturation, brightness: brightness)
} }
@MainActor
func reset() {
currentSeeds.removeAll()
entries.removeAll()
previousEntries.removeAll()
}
@MainActor @MainActor
private func rebuildEntries() { private func rebuildEntries() {
guard !currentSeeds.isEmpty else { guard !currentSeeds.isEmpty else {
@@ -14,15 +14,15 @@ import Foundation
@MainActor @MainActor
protocol PublicMessagePipelineDelegate: AnyObject { protocol PublicMessagePipelineDelegate: AnyObject {
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date?
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date)
/// Commits a batched message to its conversation in the store. /// Commits a batched message to its conversation in the store.
/// Returns `false` when the message was already present (ID dedup). /// Returns `false` when the message was already present (ID dedup).
@discardableResult @discardableResult
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage) func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage)
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool)
} }
@MainActor @MainActor
-2
View File
@@ -15,8 +15,6 @@ struct AppInfoView: View {
AppTheme(rawValue: appThemeRawValue) ?? .matrix AppTheme(rawValue: appThemeRawValue) ?? .matrix
} }
private var backgroundColor: Color { palette.background }
private var textColor: Color { palette.primary } private var textColor: Color { palette.primary }
private var secondaryTextColor: Color { palette.secondary } 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 privateConversationModel: PrivateConversationModel
@EnvironmentObject private var verificationModel: VerificationModel @EnvironmentObject private var verificationModel: VerificationModel
@EnvironmentObject private var conversationUIModel: ConversationUIModel @EnvironmentObject private var conversationUIModel: ConversationUIModel
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
@EnvironmentObject private var peerListModel: PeerListModel
@Binding var showSidebar: Bool @Binding var showSidebar: Bool
@Binding var messageText: String @Binding var messageText: String
@@ -79,8 +77,7 @@ struct ContentPeopleSheetView: View {
#endif #endif
} else { } else {
ContentPeopleListView( ContentPeopleListView(
showSidebar: $showSidebar, showSidebar: $showSidebar
headerHeight: headerHeight
) )
} }
} }
@@ -142,8 +139,6 @@ private struct ContentPeopleListView: View {
@Binding var showSidebar: Bool @Binding var showSidebar: Bool
let headerHeight: CGFloat
@State private var showVerifySheet = false @State private var showVerifySheet = false
var body: some View { var body: some View {
@@ -285,7 +280,6 @@ private extension ContentPeopleListView {
} }
private struct ContentPrivateChatSheetView: View { private struct ContentPrivateChatSheetView: View {
@EnvironmentObject private var appChromeModel: AppChromeModel
@EnvironmentObject private var privateConversationModel: PrivateConversationModel @EnvironmentObject private var privateConversationModel: PrivateConversationModel
@Binding var showSidebar: Bool @Binding var showSidebar: Bool
-5
View File
@@ -17,8 +17,6 @@ struct FingerprintView: View {
private var textColor: Color { palette.primary } private var textColor: Color { palette.primary }
private var backgroundColor: Color { palette.background }
private enum Strings { private enum Strings {
static let title: LocalizedStringKey = "fingerprint.title" static let title: LocalizedStringKey = "fingerprint.title"
static let theirFingerprint: LocalizedStringKey = "fingerprint.their_label" static let theirFingerprint: LocalizedStringKey = "fingerprint.their_label"
@@ -45,9 +43,6 @@ struct FingerprintView: View {
count count
) )
} }
static func unknownPeer() -> String {
String(localized: "common.unknown", comment: "Label for an unknown peer")
}
} }
var body: some View { var body: some View {
+1 -3
View File
@@ -13,8 +13,6 @@ struct LocationChannelsSheet: View {
@State private var customGeohash: String = "" @State private var customGeohash: String = ""
@State private var customError: String? = nil @State private var customError: String? = nil
private var backgroundColor: Color { palette.background }
private enum Strings { private enum Strings {
static let title: LocalizedStringKey = "location_channels.title" static let title: LocalizedStringKey = "location_channels.title"
static let description: LocalizedStringKey = "location_channels.description" static let description: LocalizedStringKey = "location_channels.description"
@@ -401,7 +399,7 @@ struct LocationChannelsSheet: View {
title: String, title: String,
subtitlePrefix: String, subtitlePrefix: String,
subtitleName: String? = nil, subtitleName: String? = nil,
subtitleNameBold: Bool = false, subtitleNameBold _: Bool = false,
isSelected: Bool, isSelected: Bool,
titleColor: Color? = nil, titleColor: Color? = nil,
titleBold: Bool = false, titleBold: Bool = false,
-1
View File
@@ -288,7 +288,6 @@ struct VerificationSheetView: View {
@State private var showingScanner = false @State private var showingScanner = false
@ThemedPalette private var palette @ThemedPalette private var palette
private var backgroundColor: Color { palette.background }
private var accentColor: Color { palette.accent } private var accentColor: Color { palette.accent }
private var boxColor: Color { palette.secondary.opacity(0.1) } 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 mentionCheckedMessageIDs: [String] = []
private(set) var hapticMessageIDs: [String] = [] private(set) var hapticMessageIDs: [String] = []
func handlePublicMessage(_ message: BitchatMessage) { handledPublicMessages.append(message) }
func handlePublicMessage(_ message: BitchatMessage, powBits: Int) { handledPublicMessages.append(message) } func handlePublicMessage(_ message: BitchatMessage, powBits: Int) { handledPublicMessages.append(message) }
func checkForMentions(_ message: BitchatMessage) { mentionCheckedMessageIDs.append(message.id) } func checkForMentions(_ message: BitchatMessage) { mentionCheckedMessageIDs.append(message.id) }
func sendHapticFeedback(for message: BitchatMessage) { hapticMessageIDs.append(message.id) } func sendHapticFeedback(for message: BitchatMessage) { hapticMessageIDs.append(message.id) }
@@ -214,8 +213,6 @@ private final class MockChatNostrContext: ChatNostrContext {
// Favorites & notifications // Favorites & notifications
var favoriteRelationshipsByNoiseKey: [Data: FavoritesPersistenceService.FavoriteRelationship] = [:] 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)] = [] private(set) var geohashActivityNotifications: [(geohash: String, bodyPreview: String)] = []
func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship? { func favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship? {
@@ -226,14 +223,6 @@ private final class MockChatNostrContext: ChatNostrContext {
Array(favoriteRelationshipsByNoiseKey.values) 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) { func notifyGeohashActivity(geohash: String, bodyPreview: String) {
geohashActivityNotifications.append((geohash, bodyPreview)) 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 // MARK: - Coordinator Tests Against Mock Context
/// Exercises `ChatNostrCoordinator` against `MockChatNostrContext` with no /// Exercises `ChatNostrCoordinator` against `MockChatNostrContext` with no
@@ -29,7 +29,6 @@ private final class MockChatPeerIdentityContext: ChatPeerIdentityContext {
var unreadPrivateMessages: Set<PeerID> = [] var unreadPrivateMessages: Set<PeerID> = []
var selectedPrivateChatPeer: PeerID? var selectedPrivateChatPeer: PeerID?
var selectedPrivateChatFingerprint: String? var selectedPrivateChatFingerprint: String?
var nickname = "me"
var myPeerID = PeerID(str: "0011223344556677") var myPeerID = PeerID(str: "0011223344556677")
var activeChannel: ChannelID = .mesh var activeChannel: ChannelID = .mesh
private(set) var notifyUIChangedCount = 0 private(set) var notifyUIChangedCount = 0
@@ -100,11 +100,6 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
unreadPrivateMessages.remove(peerID) unreadPrivateMessages.remove(peerID)
} }
func removePrivateChat(_ peerID: PeerID) {
privateChats.removeValue(forKey: peerID)
unreadPrivateMessages.remove(peerID)
}
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID) { func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID) {
migratedChats.append((oldPeerID, newPeerID)) migratedChats.append((oldPeerID, newPeerID))
guard oldPeerID != newPeerID, let source = privateChats[oldPeerID] else { return } guard oldPeerID != newPeerID, let source = privateChats[oldPeerID] else { return }
@@ -177,12 +172,10 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
// Routing & acknowledgements // Routing & acknowledgements
private(set) var routedPrivateMessages: [(content: String, peerID: PeerID, messageID: String)] = [] private(set) var routedPrivateMessages: [(content: String, peerID: PeerID, messageID: String)] = []
private(set) var routedReadReceipts: [(messageID: String, peerID: PeerID)] = [] 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 meshReadReceipts: [(messageID: String, peerID: PeerID)] = []
private(set) var geoPrivateMessages: [(content: String, recipientHex: String, messageID: String)] = [] private(set) var geoPrivateMessages: [(content: String, recipientHex: String, messageID: String)] = []
private(set) var geoDeliveryAcks: [(messageID: String, recipientHex: String)] = [] private(set) var geoDeliveryAcks: [(messageID: String, recipientHex: String)] = []
private(set) var geoReadReceipts: [(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) { func routePrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String) {
routedPrivateMessages.append((content, peerID, messageID)) routedPrivateMessages.append((content, peerID, messageID))
@@ -192,10 +185,6 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
routedReadReceipts.append((receipt.originalMessageID, peerID)) routedReadReceipts.append((receipt.originalMessageID, peerID))
} }
func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
routedFavoriteNotifications.append((peerID, isFavorite))
}
func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) { func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID) {
meshReadReceipts.append((receipt.originalMessageID, peerID)) meshReadReceipts.append((receipt.originalMessageID, peerID))
} }
@@ -212,10 +201,6 @@ private final class MockChatPrivateConversationContext: ChatPrivateConversationC
geoReadReceipts.append((messageID, recipientHex)) geoReadReceipts.append((messageID, recipientHex))
} }
func sendDeliveryAckViaNostrEmbedded(_ message: BitchatMessage, wasReadBefore: Bool, senderPubkey: String, key: Data?) {
embeddedDeliveryAckMessageIDs.append(message.id)
}
// Favorites & notifications // Favorites & notifications
var favoriteRelationshipsByNoiseKey: [Data: FavoritesPersistenceService.FavoriteRelationship] = [:] var favoriteRelationshipsByNoiseKey: [Data: FavoritesPersistenceService.FavoriteRelationship] = [:]
private(set) var peerFavoritedUsUpdates: [(noiseKey: Data, favorited: Bool, nickname: String, nostrPublicKey: String?)] = [] private(set) var peerFavoritedUsUpdates: [(noiseKey: Data, favorited: Bool, nickname: String, nostrPublicKey: String?)] = []
@@ -58,11 +58,6 @@ private final class MockChatPublicConversationContext: ChatPublicConversationCon
return true 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 { func publicConversationContainsMessage(withID messageID: String, in conversationID: ConversationID) -> Bool {
conversations[conversationID]?.contains(where: { $0.id == messageID }) == true conversations[conversationID]?.contains(where: { $0.id == messageID }) == true
} }
-4
View File
@@ -675,10 +675,6 @@ private final class MockCommandContextProvider: CommandContextProvider {
toggledFavorites.append(peerID) toggledFavorites.append(peerID)
} }
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
favoriteNotifications.append((peerID, isFavorite))
}
// Groups: record the parsed subcommand + argument the processor forwarded. // Groups: record the parsed subcommand + argument the processor forwarded.
private(set) var groupCommands: [(subcommand: String, argument: String)] = [] 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 courierSends: [(messageID: String, recipientKey: Data, couriers: [PeerID])] = []
private(set) var directSends: [String] = [] private(set) var directSends: [String] = []
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
Just(snapshots).eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { snapshots } func currentPeerSnapshots() -> [TransportPeerSnapshot] { snapshots }
var myPeerID = PeerID(str: "00000000000000aa") var myPeerID = PeerID(str: "00000000000000aa")
@@ -18,9 +18,7 @@ struct PublicChatE2ETests {
private let charlie: MockBLEService private let charlie: MockBLEService
private let david: MockBLEService private let david: MockBLEService
private let bus = MockBLEBus() private let bus = MockBLEBus()
private var receivedMessages: [String: [BitchatMessage]] = [:]
init() { init() {
// Create mock services with unique peer IDs to avoid any collision // Create mock services with unique peer IDs to avoid any collision
alice = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname1, bus: bus) alice = MockBLEService(peerID: PeerID(str: UUID().uuidString), nickname: TestConstants.testNickname1, bus: bus)
-1
View File
@@ -1,6 +1,5 @@
import SwiftUI import SwiftUI
import XCTest import XCTest
@testable import bitchat
final class FontBitchatTests: XCTestCase { final class FontBitchatTests: XCTestCase {
// func testMonospacedMapping() { // func testMonospacedMapping() {
@@ -369,7 +369,6 @@ extension FragmentationTests {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {} func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {}
func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {} func didReceiveNoisePayload(from peerID: PeerID, type: NoisePayloadType, payload: Data, timestamp: Date) {}
func didUpdateBluetoothState(_ state: CBManagerState) {} func didUpdateBluetoothState(_ state: CBManagerState) {}
func didReceiveRegionalPublicMessage(from peerID: PeerID, nickname: String, content: String, timestamp: Date) {}
} }
// Helper: build a large message packet (unencrypted public message) // Helper: build a large message packet (unencrypted public message)
@@ -33,14 +33,6 @@ final class TestNetworkHelper {
return node return node
} }
func getNode(_ name: String) -> MockBLEService? {
nodes[name]
}
func getManager(_ name: String) -> NoiseSessionManager? {
noiseManagers[name]
}
// MARK: - Topology // MARK: - Topology
func connect(_ a: String, _ b: String) { func connect(_ a: String, _ b: String) {
+1 -3
View File
@@ -10,7 +10,6 @@ struct LocalizationCoverageTests {
.deletingLastPathComponent() // repo root .deletingLastPathComponent() // repo root
private struct Catalog { private struct Catalog {
let sourceLanguage: String
/// key -> set of locales with a localization entry /// key -> set of locales with a localization entry
let coverage: [String: Set<String>] let coverage: [String: Set<String>]
/// all locales appearing anywhere in the catalog /// all locales appearing anywhere in the catalog
@@ -22,7 +21,6 @@ struct LocalizationCoverageTests {
let data = try Data(contentsOf: url) let data = try Data(contentsOf: url)
let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any]) let root = try #require(try JSONSerialization.jsonObject(with: data) as? [String: Any])
let strings = try #require(root["strings"] 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>] = [:] var coverage: [String: Set<String>] = [:]
for (key, value) in strings { for (key, value) in strings {
@@ -43,7 +41,7 @@ struct LocalizationCoverageTests {
} }
coverage[key] = locales coverage[key] = locales
} }
return Catalog(sourceLanguage: sourceLanguage, coverage: coverage) return Catalog(coverage: coverage)
} }
@Test func mainCatalogCoversAllLocalesForEveryKey() throws { @Test func mainCatalogCoversAllLocalesForEveryKey() throws {
-1
View File
@@ -8,7 +8,6 @@
import Foundation import Foundation
import BitFoundation import BitFoundation
@testable import bitchat
final class MockBLEBus { final class MockBLEBus {
private var registry: [PeerID: MockBLEService] = [:] private var registry: [PeerID: MockBLEService] = [:]
-62
View File
@@ -48,10 +48,6 @@ final class MockBLEService: NSObject {
set { myNickname = newValue } set { myNickname = newValue }
} }
var nickname: String {
return myNickname
}
var peerID: PeerID { var peerID: PeerID {
return myPeerID return myPeerID
} }
@@ -62,12 +58,6 @@ final class MockBLEService: NSObject {
self.bus = bus self.bus = bus
} }
// MARK: - Methods matching BLEService
func setNickname(_ nickname: String) {
self.myNickname = nickname
}
// MARK: - In-memory test bus (for E2E/Integration) // MARK: - In-memory test bus (for E2E/Integration)
/// Registers this instance on first use. /// Registers this instance on first use.
@@ -92,10 +82,6 @@ final class MockBLEService: NSObject {
return connectedPeers.contains(peerID) return connectedPeers.contains(peerID)
} }
func peerNickname(peerID: String) -> String? {
"MockPeer_\(peerID)"
}
func getPeerNicknames() -> [PeerID: String] { func getPeerNicknames() -> [PeerID: String] {
var nicknames: [PeerID: String] = [:] var nicknames: [PeerID: String] = [:]
for peer in connectedPeers { for peer in connectedPeers {
@@ -103,10 +89,6 @@ final class MockBLEService: NSObject {
} }
return nicknames return nicknames
} }
func getPeers() -> [PeerID: String] {
return getPeerNicknames()
}
/// Keep local echo synchronous so Swift Testing confirmations observe it deterministically. /// Keep local echo synchronous so Swift Testing confirmations observe it deterministically.
private func deliverLocalEcho(_ message: BitchatMessage) { private func deliverLocalEcho(_ message: BitchatMessage) {
@@ -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) { func sendPrivateMessage(_ content: String, to recipientPeerID: PeerID, recipientNickname: String, messageID: String) {
let message = BitchatMessage( let message = BitchatMessage(
id: messageID, 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 // MARK: - Test Helper Methods
func simulateConnectedPeer(_ peerID: PeerID) { func simulateConnectedPeer(_ peerID: PeerID) {
@@ -314,9 +255,6 @@ final class MockBLEService: NSObject {
} }
} }
// Backward compatibility for older tests
typealias MockSimplifiedBluetoothService = MockBLEService
// MARK: - Helpers // MARK: - Helpers
extension MockBLEService { extension MockBLEService {
+3 -22
View File
@@ -11,18 +11,11 @@ import BitFoundation
@testable import bitchat @testable import bitchat
final class MockIdentityManager: SecureIdentityStateManagerProtocol { final class MockIdentityManager: SecureIdentityStateManagerProtocol {
private let keychain: KeychainManagerProtocol
private var blockedFingerprints: Set<String> = [] private var blockedFingerprints: Set<String> = []
private var blockedNostrPubkeys: Set<String> = [] private var blockedNostrPubkeys: Set<String> = []
private var socialIdentities: [String: SocialIdentity] = [:] private var socialIdentities: [String: SocialIdentity] = [:]
init(_ keychain: KeychainManagerProtocol) { init(_: KeychainManagerProtocol) {}
self.keychain = keychain
}
func loadIdentityCache() {}
func saveIdentityCache() {}
func forceSave() {} 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 { func isFavorite(fingerprint: String) -> Bool {
false false
} }
@@ -99,9 +86,7 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol {
} }
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {} func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
func updateHandshakeState(peerID: PeerID, state: HandshakeState) {}
func clearAllIdentityData() {} func clearAllIdentityData() {}
func removeEphemeralSession(peerID: PeerID) {} func removeEphemeralSession(peerID: PeerID) {}
@@ -139,10 +124,6 @@ final class MockIdentityManager: SecureIdentityStateManagerProtocol {
!(vouchesByVouchee[fingerprint] ?? []).isEmpty !(vouchesByVouchee[fingerprint] ?? []).isEmpty
} }
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel {
socialIdentities[fingerprint]?.trustLevel ?? .unknown
}
func lastVouchBatchSent(to fingerprint: String) -> Date? { func lastVouchBatchSent(to fingerprint: String) -> Date? {
vouchBatchSentAt[fingerprint] vouchBatchSentAt[fingerprint]
} }
-3
View File
@@ -26,9 +26,6 @@ final class MockTransport: Transport {
var myNickname: String = "TestUser" var myNickname: String = "TestUser"
private let peerSnapshotSubject = CurrentValueSubject<[TransportPeerSnapshot], Never>([]) private let peerSnapshotSubject = CurrentValueSubject<[TransportPeerSnapshot], Never>([])
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
peerSnapshotSubject.eraseToAnyPublisher()
}
// MARK: - Recording Properties (for test assertions) // MARK: - Recording Properties (for test assertions)
@@ -31,11 +31,8 @@ struct NoiseTestVector: Codable {
let init_prologue: String let init_prologue: String
let init_static: String let init_static: String
let init_ephemeral: String let init_ephemeral: String
let init_psks: [String]?
let resp_prologue: String
let resp_static: String let resp_static: String
let resp_ephemeral: String let resp_ephemeral: String
let resp_psks: [String]?
let handshake_hash: String? let handshake_hash: String?
let messages: [TestMessage] let messages: [TestMessage]
+1
View File
@@ -156,6 +156,7 @@ struct NostrProtocolTests {
} }
} }
@Test
func testAckRoundTripNIP44V2_Delivered() throws { func testAckRoundTripNIP44V2_Delivered() throws {
// Identities // Identities
let sender = try NostrIdentity.generate() let sender = try NostrIdentity.generate()
@@ -118,6 +118,7 @@ struct NotificationStreamAssemblerTests {
#expect(decoded.timestamp == packet2.timestamp) #expect(decoded.timestamp == packet2.timestamp)
} }
@Test
func testAssemblesCompressedLargeFrame() throws { func testAssemblesCompressedLargeFrame() throws {
var assembler = NotificationStreamAssembler() var assembler = NotificationStreamAssembler()
@@ -610,7 +610,6 @@ private final class PerfNostrContext: ChatNostrContext {
func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool { true } func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool { true }
private(set) var handledPublicMessageCount = 0 private(set) var handledPublicMessageCount = 0
func handlePublicMessage(_ message: BitchatMessage) { handledPublicMessageCount += 1 }
func handlePublicMessage(_ message: BitchatMessage, powBits: Int) { handledPublicMessageCount += 1 } func handlePublicMessage(_ message: BitchatMessage, powBits: Int) { handledPublicMessageCount += 1 }
func checkForMentions(_ message: BitchatMessage) {} func checkForMentions(_ message: BitchatMessage) {}
func sendHapticFeedback(for 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 favoriteRelationship(forNoiseKey noiseKey: Data) -> FavoritesPersistenceService.FavoriteRelationship? { nil }
func allFavoriteRelationships() -> [FavoritesPersistenceService.FavoriteRelationship] { [] } 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) {} func notifyGeohashActivity(geohash: String, bodyPreview: String) {}
} }
@@ -779,7 +776,6 @@ private final class PerfDeliveryContext: ChatDeliveryContext {
@MainActor @MainActor
private final class PerfPipelineFixture { private final class PerfPipelineFixture {
let viewModel: ChatViewModel let viewModel: ChatViewModel
let transport: MockTransport
let conversations: ConversationStore let conversations: ConversationStore
let privateInbox: PrivateInboxModel let privateInbox: PrivateInboxModel
let publicChat: PublicChatModel let publicChat: PublicChatModel
@@ -791,7 +787,6 @@ private final class PerfPipelineFixture {
let transport = MockTransport() let transport = MockTransport()
let conversations = ConversationStore() let conversations = ConversationStore()
self.transport = transport
self.conversations = conversations self.conversations = conversations
self.viewModel = ChatViewModel( self.viewModel = ChatViewModel(
keychain: keychain, keychain: keychain,
-4
View File
@@ -23,10 +23,6 @@ private final class DefaultTransportProbe: Transport {
var myNickname = "Tester" var myNickname = "Tester"
private(set) var sentMessages: [(content: String, mentions: [String])] = [] private(set) var sentMessages: [(content: String, mentions: [String])] = []
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
subject.eraseToAnyPublisher()
}
func currentPeerSnapshots() -> [TransportPeerSnapshot] { subject.value } func currentPeerSnapshots() -> [TransportPeerSnapshot] { subject.value }
func setNickname(_ nickname: String) { myNickname = nickname } func setNickname(_ nickname: String) { myNickname = nickname }
func startServices() {} func startServices() {}
@@ -27,28 +27,28 @@ private final class TestPipelineDelegate: PublicMessagePipelineDelegate {
committed.filter { $0.conversationID == conversationID }.map(\.message) 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) dedupService.normalizedContentKey(content)
} }
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? { func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
dedupService.contentTimestamp(forKey: key) 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) dedupService.recordContentKey(key, timestamp: timestamp)
recordedContentKeys.append(key) 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 } guard !rejectedMessageIDs.contains(message.id) else { return false }
committed.append((message, conversationID)) committed.append((message, conversationID))
return true 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) batchingStates.append(isBatching)
} }
} }
@@ -230,8 +230,4 @@ private final class MockGeohashPresenceTimer: GeohashPresenceTimerProtocol {
invalidateCallCount += 1 invalidateCallCount += 1
isValid = false isValid = false
} }
func fire() {
handler()
}
} }
@@ -120,7 +120,6 @@ final class NetworkActivationServiceTests: XCTestCase {
return NetworkActivationTestContext( return NetworkActivationTestContext(
service: service, service: service,
storage: storage, storage: storage,
permissionSubject: permissionSubject,
favoritesSubject: favoritesSubject, favoritesSubject: favoritesSubject,
torController: torController, torController: torController,
relayController: relayController, relayController: relayController,
@@ -148,7 +147,6 @@ final class NetworkActivationServiceTests: XCTestCase {
private struct NetworkActivationTestContext { private struct NetworkActivationTestContext {
let service: NetworkActivationService let service: NetworkActivationService
let storage: UserDefaults let storage: UserDefaults
let permissionSubject: CurrentValueSubject<LocationChannelManager.PermissionState, Never>
let favoritesSubject: CurrentValueSubject<Set<Data>, Never> let favoritesSubject: CurrentValueSubject<Set<Data>, Never>
let torController: MockNetworkActivationTorController let torController: MockNetworkActivationTorController
let relayController: MockNetworkActivationRelayController let relayController: MockNetworkActivationRelayController
@@ -1483,7 +1483,6 @@ final class NostrRelayManagerTests: XCTestCase {
return RelayManagerTestContext( return RelayManagerTestContext(
manager: manager, manager: manager,
permissionSubject: permissionSubject, permissionSubject: permissionSubject,
favoritesSubject: favoritesSubject,
sessionFactory: sessionFactory, sessionFactory: sessionFactory,
scheduler: scheduler, scheduler: scheduler,
clock: clock, clock: clock,
@@ -1537,7 +1536,6 @@ final class NostrRelayManagerTests: XCTestCase {
private struct RelayManagerTestContext { private struct RelayManagerTestContext {
let manager: NostrRelayManager let manager: NostrRelayManager
let permissionSubject: CurrentValueSubject<LocationChannelManager.PermissionState, Never> let permissionSubject: CurrentValueSubject<LocationChannelManager.PermissionState, Never>
let favoritesSubject: CurrentValueSubject<Set<Data>, Never>
let sessionFactory: MockRelaySessionFactory let sessionFactory: MockRelaySessionFactory
let scheduler: MockRelayScheduler let scheduler: MockRelayScheduler
let clock: MutableClock let clock: MutableClock
@@ -1644,7 +1642,6 @@ private final class MockRelaySessionFactory: NostrRelaySessionProtocol {
} }
private final class MockRelayConnection: NostrRelayConnectionProtocol { private final class MockRelayConnection: NostrRelayConnectionProtocol {
private let url: String
private let pingError: Error? private let pingError: Error?
private let sendError: Error? private let sendError: Error?
private var receiveHandler: ((Result<URLSessionWebSocketTask.Message, Error>) -> Void)? 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) { init(url _: String, pingError: Error? = nil, sendError: Error? = nil) {
self.url = url
self.pingError = pingError self.pingError = pingError
self.sendError = sendError self.sendError = sendError
} }
@@ -214,18 +214,6 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
socialIdentities[identity.fingerprint] = identity 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 { func isFavorite(fingerprint: String) -> Bool {
favorites.contains(fingerprint) favorites.contains(fingerprint)
} }
@@ -266,8 +254,6 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {} func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
func updateHandshakeState(peerID: PeerID, state: HandshakeState) {}
func clearAllIdentityData() { func clearAllIdentityData() {
socialIdentities.removeAll() socialIdentities.removeAll()
favorites.removeAll() favorites.removeAll()
@@ -308,10 +294,6 @@ private final class TestIdentityManager: SecureIdentityStateManagerProtocol {
false false
} }
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel {
verified.contains(fingerprint) ? .verified : .unknown
}
func lastVouchBatchSent(to fingerprint: String) -> Date? { func lastVouchBatchSent(to fingerprint: String) -> Date? {
nil nil
} }
@@ -25,9 +25,5 @@ struct TestConstants {
static let testNickname4 = "David" static let testNickname4 = "David"
static let testMessage1 = "Hello, World!" 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 testLongMessage = String(repeating: "This is a long message. ", count: 100)
static let testSignature = Data(repeating: 0xAB, count: 64)
} }
+2 -53
View File
@@ -12,20 +12,7 @@ import BitFoundation
@testable import bitchat @testable import bitchat
final class TestHelpers { 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 // MARK: - Message Creation
static func createTestMessage( static func createTestMessage(
@@ -78,11 +65,7 @@ final class TestHelpers {
} }
return data return data
} }
static func generateTestPeerID() -> String {
return "PEER" + UUID().uuidString.prefix(8)
}
// MARK: - Async Helpers // MARK: - Async Helpers
static func waitFor(_ condition: @escaping () -> Bool, timeout: TimeInterval = TestConstants.defaultTimeout) async throws { static func waitFor(_ condition: @escaping () -> Bool, timeout: TimeInterval = TestConstants.defaultTimeout) async throws {
@@ -110,32 +93,10 @@ final class TestHelpers {
} }
return true 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 { enum TestError: Error {
case timeout case timeout
case unexpectedValue
case testFailure(String)
} }
// MARK: - Private chat seeding (ConversationStore migration) // 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 { func sleep(_ seconds: TimeInterval) async throws {
@@ -30,12 +30,6 @@ private func arti_bootstrap_progress() -> Int32
@_silgen_name("arti_bootstrap_summary") @_silgen_name("arti_bootstrap_summary")
private func arti_bootstrap_summary(_ buf: UnsafeMutablePointer<CChar>, _ len: Int32) -> Int32 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. /// Arti-based Tor integration for BitChat.
/// - Boots a local Arti client and exposes a SOCKS5 proxy /// - Boots a local Arti client and exposes a SOCKS5 proxy
/// on 127.0.0.1:socksPort. All app networking should await readiness and /// 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 bootstrapMonitorStarted = false
private var pathMonitor: NWPathMonitor? private var pathMonitor: NWPathMonitor?
private var isAppForeground: Bool = true private var isAppForeground: Bool = true
private var isDormant: Bool = false
private var lastRestartAt: Date? = nil private var lastRestartAt: Date? = nil
private var startedAt: Date? = nil // Tracks initial startup time for grace period private var startedAt: Date? = nil // Tracks initial startup time for grace period
private(set) var allowAutoStart: Bool = false private(set) var allowAutoStart: Bool = false
@@ -102,7 +95,6 @@ public final class TorManager: ObservableObject {
} }
guard !didStart else { return } guard !didStart else { return }
didStart = true didStart = true
isDormant = false
isStarting = true isStarting = true
startedAt = Date() // Track startup time for grace period startedAt = Date() // Track startup time for grace period
SecureLogger.debug("TorManager: startIfNeeded() - startedAt set", category: .session) SecureLogger.debug("TorManager: startIfNeeded() - startedAt set", category: .session)
@@ -353,7 +345,6 @@ public final class TorManager: ObservableObject {
} }
await MainActor.run { await MainActor.run {
self.isDormant = false
self.isReady = false self.isReady = false
self.socksReady = false self.socksReady = false
self.bootstrapProgress = 0 self.bootstrapProgress = 0
@@ -383,7 +374,6 @@ public final class TorManager: ObservableObject {
self.bootstrapProgress = 0 self.bootstrapProgress = 0
self.bootstrapSummary = "" self.bootstrapSummary = ""
self.isStarting = true self.isStarting = true
self.isDormant = false
self.lastRestartAt = Date() self.lastRestartAt = Date()
} }
@@ -105,10 +105,6 @@ public struct BinaryProtocol {
// Field offsets within packet header // Field offsets within packet header
public struct Offsets { 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) 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.Data
import struct Foundation.Date
/// The core packet structure for all BitChat protocol messages. /// The core packet structure for all BitChat protocol messages.
/// Encapsulates all data needed for routing through the mesh network, /// Encapsulates all data needed for routing through the mesh network,
@@ -37,35 +36,7 @@ public struct BitchatPacket: Codable {
self.route = route self.route = route
self.isRSR = isRSR 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? { public func toBinaryData(padding: Bool = true) -> Data? {
BinaryProtocol.encode(self, padding: padding) BinaryProtocol.encode(self, padding: padding)
} }

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