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