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