mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 14:25:20 +00:00
Remove dead code found by full Periphery audit; add scan config + advisory CI (#1410)
Periphery 3.7.4 audit of both schemes (macOS + iOS, intersected so platform-specific code is never touched), with test targets indexed and the share extension built. 277 dead declarations removed or demoted: dead forwarding wrappers (ChatViewModel+Nostr/+PrivateChat), removed- feature remnants (autocomplete command suggestions, back-swipe tuning, MediaSendError, GeohashParticipantTracker), unused Tor dormancy bindings, assign-only properties, unused parameters (renamed to _), and redundant public accessibility. 13 orphaned localization keys deleted across all 29 locales (old pre-#1392 location-notes UI, app_info warnings). Two real tests were flagged as unused because they never ran: Swift Testing methods missing @Test (NostrProtocolTests. testAckRoundTripNIP44V2_Delivered, NotificationStreamAssemblerTests. testAssemblesCompressedLargeFrame). Re-armed both; they pass. Deliberately kept, now recorded in .periphery.baseline.json: iOS-only code invisible to the CI macOS scan, C FFI signatures, keep-alive NWPathMonitor reference, InboundEventKey.eventID (dedup semantics), wifiBulk capability bit (reserved for Wi-Fi bulk work, used by BitFoundation package tests), and the String secureClear cluster (exercised by package tests). New: .periphery.yml config and an advisory Dead Code CI job (mirrors the SwiftLint precedent from #1361) that fails on findings not in the committed baseline. Verified: full macOS app suite, BitFoundation (119) and BitLogger (13) package tests green; periphery scan --strict exits clean. Co-authored-by: jack <jackjackbits@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
jack
Claude Fable 5
parent
d5cf64f99e
commit
ef848857b7
@@ -36,34 +36,12 @@ enum AppEvent: Sendable, Equatable {
|
||||
actor AppEventStream {
|
||||
private var continuations: [UUID: AsyncStream<AppEvent>.Continuation] = [:]
|
||||
|
||||
func stream() -> AsyncStream<AppEvent> {
|
||||
let id = UUID()
|
||||
return AsyncStream { continuation in
|
||||
continuations[id] = continuation
|
||||
continuation.onTermination = { [id] _ in
|
||||
Task {
|
||||
await self.removeContinuation(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func emit(_ event: AppEvent) {
|
||||
for continuation in continuations.values {
|
||||
continuation.yield(event)
|
||||
}
|
||||
}
|
||||
|
||||
func finish() {
|
||||
for continuation in continuations.values {
|
||||
continuation.finish()
|
||||
}
|
||||
continuations.removeAll()
|
||||
}
|
||||
|
||||
private func removeContinuation(_ id: UUID) {
|
||||
continuations.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Identity key for a direct conversation. Equality and hashing use the
|
||||
|
||||
@@ -18,8 +18,6 @@ final class AppRuntime: ObservableObject {
|
||||
/// (docs/CONVERSATION-STORE-DESIGN.md). Owned here; the feature models
|
||||
/// and `ChatViewModel` observe and mutate it through its intent API.
|
||||
let conversations: ConversationStore
|
||||
let peerIdentityStore: PeerIdentityStore
|
||||
let locationPresenceStore: LocationPresenceStore
|
||||
let publicChatModel: PublicChatModel
|
||||
let privateInboxModel: PrivateInboxModel
|
||||
let privateConversationModel: PrivateConversationModel
|
||||
@@ -51,8 +49,6 @@ final class AppRuntime: ObservableObject {
|
||||
let locationPresenceStore = LocationPresenceStore()
|
||||
let locationManager = LocationChannelManager.shared
|
||||
self.conversations = conversations
|
||||
self.peerIdentityStore = peerIdentityStore
|
||||
self.locationPresenceStore = locationPresenceStore
|
||||
self.chatViewModel = ChatViewModel(
|
||||
keychain: keychain,
|
||||
idBridge: idBridge,
|
||||
|
||||
@@ -796,16 +796,6 @@ extension ConversationStore {
|
||||
return messageIDs
|
||||
}
|
||||
|
||||
/// Removes every direct conversation (panic clear).
|
||||
func removeAllDirectConversations() {
|
||||
let directIDs = conversationIDs.filter { id in
|
||||
if case .direct = id { return true }
|
||||
return false
|
||||
}
|
||||
for id in directIDs {
|
||||
removeConversation(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Diagnostics support
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import BitFoundation
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@@ -17,7 +16,6 @@ final class LocationChannelsModel: ObservableObject {
|
||||
private let manager: LocationChannelManager
|
||||
private let network: NetworkActivationService
|
||||
private let gateway: GatewayService
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
manager: LocationChannelManager? = nil,
|
||||
|
||||
@@ -25,10 +25,6 @@ final class PeerIdentityStore: ObservableObject {
|
||||
stablePeerIDsByShortID[peerID] = stablePeerID
|
||||
}
|
||||
|
||||
func replaceStablePeerIDs(_ mappings: [PeerID: PeerID]) {
|
||||
stablePeerIDsByShortID = mappings
|
||||
}
|
||||
|
||||
func fingerprint(for peerID: PeerID) -> String? {
|
||||
peerFingerprintsByPeerID[peerID]
|
||||
}
|
||||
@@ -94,10 +90,6 @@ final class PeerIdentityStore: ObservableObject {
|
||||
invalidateEncryptionCache(for: peerID)
|
||||
}
|
||||
|
||||
func replaceEncryptionStatuses(_ statuses: [PeerID: EncryptionStatus]) {
|
||||
encryptionStatuses = statuses
|
||||
}
|
||||
|
||||
func setVerifiedFingerprints(_ fingerprints: Set<String>) {
|
||||
verifiedFingerprints = fingerprints
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
struct FingerprintPresentationState: Equatable {
|
||||
let statusPeerID: PeerID
|
||||
let peerNickname: String
|
||||
let encryptionStatus: EncryptionStatus
|
||||
let theirFingerprint: String?
|
||||
@@ -56,10 +55,6 @@ final class VerificationModel: ObservableObject {
|
||||
return VerificationService.shared.buildMyQRString(nickname: currentNickname, npub: npub) ?? ""
|
||||
}
|
||||
|
||||
func beginQRVerification(with qr: VerificationService.VerificationQR) -> Bool {
|
||||
chatViewModel.beginQRVerification(with: qr)
|
||||
}
|
||||
|
||||
func verifyScannedPayload(_ payload: String) -> VerificationScanOutcome {
|
||||
guard let qr = VerificationService.shared.verifyScannedQR(payload) else {
|
||||
return .invalid
|
||||
@@ -110,7 +105,6 @@ final class VerificationModel: ObservableObject {
|
||||
}
|
||||
|
||||
return FingerprintPresentationState(
|
||||
statusPeerID: statusPeerID,
|
||||
peerNickname: peerNickname,
|
||||
encryptionStatus: encryptionStatus,
|
||||
theirFingerprint: theirFingerprint,
|
||||
|
||||
@@ -5,7 +5,6 @@ import AVFoundation
|
||||
actor VoiceRecorder {
|
||||
enum RecorderError: Error {
|
||||
case microphoneAccessDenied
|
||||
case recorderInitializationFailed
|
||||
case recordingInProgress
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +56,6 @@ final class WaveformCache {
|
||||
}
|
||||
}
|
||||
|
||||
func purgeAll() {
|
||||
queue.async(flags: .barrier) { [weak self] in
|
||||
self?.cache.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
private func computeWaveform(url: URL, bins: Int) -> [Float]? {
|
||||
guard bins > 0 else { return nil }
|
||||
// Use autoreleasepool to manage memory from audio buffer allocations
|
||||
|
||||
@@ -88,8 +88,6 @@ import BitFoundation
|
||||
/// Represents the ephemeral layer of identity - short-lived peer IDs that provide network privacy.
|
||||
/// These IDs rotate periodically to prevent tracking while maintaining cryptographic relationships.
|
||||
struct EphemeralIdentity {
|
||||
let peerID: PeerID // 8 random bytes
|
||||
let sessionStart: Date
|
||||
var handshakeState: HandshakeState
|
||||
}
|
||||
|
||||
@@ -98,7 +96,6 @@ enum HandshakeState {
|
||||
case initiated
|
||||
case inProgress
|
||||
case completed(fingerprint: String)
|
||||
case failed(reason: String)
|
||||
}
|
||||
|
||||
/// Represents the cryptographic layer of identity - the stable Noise Protocol static key pair.
|
||||
@@ -110,7 +107,6 @@ struct CryptographicIdentity: Codable {
|
||||
// Optional Ed25519 signing public key (used to authenticate public messages)
|
||||
var signingPublicKey: Data? = nil
|
||||
let firstSeen: Date
|
||||
let lastHandshake: Date?
|
||||
}
|
||||
|
||||
/// Represents the social layer of identity - user-assigned names and trust relationships.
|
||||
@@ -193,9 +189,6 @@ struct IdentityCache: Codable {
|
||||
// Fingerprint -> when we verified it (orders outgoing vouch batches;
|
||||
// entries verified before this field exists sort as oldest)
|
||||
var verifiedAt: [String: Date]? = nil
|
||||
|
||||
// Schema version for future migrations
|
||||
var version: Int = 1
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -108,8 +108,6 @@ protocol SecureIdentityStateManagerProtocol {
|
||||
func updateSocialIdentity(_ identity: SocialIdentity)
|
||||
|
||||
// MARK: Favorites Management
|
||||
func getFavorites() -> Set<String>
|
||||
func setFavorite(_ fingerprint: String, isFavorite: Bool)
|
||||
func isFavorite(fingerprint: String) -> Bool
|
||||
|
||||
// MARK: Blocked Users Management
|
||||
@@ -123,8 +121,7 @@ protocol SecureIdentityStateManagerProtocol {
|
||||
|
||||
// MARK: Ephemeral Session Management
|
||||
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState)
|
||||
func updateHandshakeState(peerID: PeerID, state: HandshakeState)
|
||||
|
||||
|
||||
// MARK: Cleanup
|
||||
func clearAllIdentityData()
|
||||
func removeEphemeralSession(peerID: PeerID)
|
||||
@@ -139,7 +136,6 @@ protocol SecureIdentityStateManagerProtocol {
|
||||
func recordVouch(voucheeFingerprint: String, voucherFingerprint: String, timestamp: Date) -> Bool
|
||||
func validVouchers(for fingerprint: String) -> [VouchRecord]
|
||||
func isVouched(fingerprint: String) -> Bool
|
||||
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel
|
||||
func lastVouchBatchSent(to fingerprint: String) -> Date?
|
||||
func markVouchBatchSent(to fingerprint: String, at date: Date)
|
||||
func signingPublicKey(forFingerprint fingerprint: String) -> Data?
|
||||
@@ -322,21 +318,13 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
||||
fingerprint: fingerprint,
|
||||
publicKey: noisePublicKey,
|
||||
signingPublicKey: signingPublicKey ?? existing.signingPublicKey,
|
||||
firstSeen: existing.firstSeen,
|
||||
lastHandshake: now
|
||||
firstSeen: existing.firstSeen
|
||||
)
|
||||
self.cryptographicIdentities[fingerprint] = existing
|
||||
} else {
|
||||
// Update signing key and lastHandshake
|
||||
// Update signing key
|
||||
existing.signingPublicKey = signingPublicKey ?? existing.signingPublicKey
|
||||
let updated = CryptographicIdentity(
|
||||
fingerprint: existing.fingerprint,
|
||||
publicKey: existing.publicKey,
|
||||
signingPublicKey: existing.signingPublicKey,
|
||||
firstSeen: existing.firstSeen,
|
||||
lastHandshake: now
|
||||
)
|
||||
self.cryptographicIdentities[fingerprint] = updated
|
||||
self.cryptographicIdentities[fingerprint] = existing
|
||||
}
|
||||
// Persist updated state (already assigned in branches above)
|
||||
} else {
|
||||
@@ -345,8 +333,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
||||
fingerprint: fingerprint,
|
||||
publicKey: noisePublicKey,
|
||||
signingPublicKey: signingPublicKey,
|
||||
firstSeen: now,
|
||||
lastHandshake: now
|
||||
firstSeen: now
|
||||
)
|
||||
self.cryptographicIdentities[fingerprint] = entry
|
||||
}
|
||||
@@ -511,11 +498,7 @@ final class SecureIdentityStateManager: SecureIdentityStateManagerProtocol {
|
||||
|
||||
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState = .none) {
|
||||
queue.async(flags: .barrier) {
|
||||
self.ephemeralSessions[peerID] = EphemeralIdentity(
|
||||
peerID: peerID,
|
||||
sessionStart: Date(),
|
||||
handshakeState: handshakeState
|
||||
)
|
||||
self.ephemeralSessions[peerID] = EphemeralIdentity(handshakeState: handshakeState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,13 +13,6 @@ extension BitchatMessage {
|
||||
enum Media {
|
||||
case voice(URL)
|
||||
case image(URL)
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .voice(let url), .image(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the directory lookup to avoid repeated FileManager calls during view rendering
|
||||
|
||||
@@ -7,7 +7,6 @@ struct BitchatPeer: Equatable {
|
||||
let peerID: PeerID // Hex-encoded peer ID
|
||||
let noisePublicKey: Data
|
||||
let nickname: String
|
||||
let lastSeen: Date
|
||||
let isConnected: Bool
|
||||
let isReachable: Bool
|
||||
|
||||
@@ -77,14 +76,13 @@ struct BitchatPeer: Equatable {
|
||||
peerID: PeerID,
|
||||
noisePublicKey: Data,
|
||||
nickname: String,
|
||||
lastSeen: Date = Date(),
|
||||
lastSeen _: Date = Date(),
|
||||
isConnected: Bool = false,
|
||||
isReachable: Bool = false
|
||||
) {
|
||||
self.peerID = peerID
|
||||
self.noisePublicKey = noisePublicKey
|
||||
self.nickname = nickname
|
||||
self.lastSeen = lastSeen
|
||||
self.isConnected = isConnected
|
||||
self.isReachable = isReachable
|
||||
|
||||
|
||||
@@ -723,7 +723,7 @@ final class NoiseHandshakeState {
|
||||
return messageBuffer
|
||||
}
|
||||
|
||||
func readMessage(_ message: Data, expectedPayloadLength: Int = 0) throws -> Data {
|
||||
func readMessage(_ message: Data, expectedPayloadLength _: Int = 0) throws -> Data {
|
||||
|
||||
guard currentPattern < messagePatterns.count else {
|
||||
throw NoiseError.handshakeComplete
|
||||
|
||||
@@ -21,12 +21,6 @@ enum NoiseSecurityConstants {
|
||||
// Maximum number of messages before rekey (2^64 - 1 is the nonce limit)
|
||||
static let maxMessagesPerSession: UInt64 = 1_000_000_000 // 1 billion messages
|
||||
|
||||
// Handshake timeout - abandon incomplete handshakes
|
||||
static let handshakeTimeout: TimeInterval = 60 // 1 minute
|
||||
|
||||
// Maximum concurrent sessions per peer
|
||||
static let maxSessionsPerPeer = 3
|
||||
|
||||
// Rate limiting
|
||||
static let maxHandshakesPerMinute = 10
|
||||
static let maxMessagesPerSecond = 100
|
||||
|
||||
@@ -14,5 +14,4 @@ enum NoiseSecurityError: Error {
|
||||
case messageTooLarge
|
||||
case invalidPeerID
|
||||
case rateLimitExceeded
|
||||
case handshakeTimeout
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ import BitFoundation
|
||||
|
||||
final class NoiseSessionManager {
|
||||
private var sessions: [PeerID: NoiseSession] = [:]
|
||||
private let localStaticKey: Curve25519.KeyAgreement.PrivateKey
|
||||
private let keychain: KeychainManagerProtocol
|
||||
private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession
|
||||
private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent)
|
||||
|
||||
@@ -23,8 +21,6 @@ final class NoiseSessionManager {
|
||||
var onSessionFailed: ((PeerID, Error) -> Void)?
|
||||
|
||||
init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) {
|
||||
self.localStaticKey = localStaticKey
|
||||
self.keychain = keychain
|
||||
self.sessionFactory = { peerID, role in
|
||||
SecureNoiseSession(
|
||||
peerID: peerID,
|
||||
@@ -37,12 +33,10 @@ final class NoiseSessionManager {
|
||||
|
||||
#if DEBUG
|
||||
init(
|
||||
localStaticKey: Curve25519.KeyAgreement.PrivateKey,
|
||||
keychain: KeychainManagerProtocol,
|
||||
localStaticKey _: Curve25519.KeyAgreement.PrivateKey,
|
||||
keychain _: KeychainManagerProtocol,
|
||||
sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession
|
||||
) {
|
||||
self.localStaticKey = localStaticKey
|
||||
self.keychain = keychain
|
||||
self.sessionFactory = sessionFactory
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,14 +6,12 @@ struct NostrIdentity: Codable {
|
||||
let privateKey: Data
|
||||
let publicKey: Data
|
||||
let npub: String // Bech32-encoded public key
|
||||
let createdAt: Date
|
||||
|
||||
|
||||
/// Memberwise initializer
|
||||
init(privateKey: Data, publicKey: Data, npub: String, createdAt: Date) {
|
||||
init(privateKey: Data, publicKey: Data, npub: String, createdAt _: Date) {
|
||||
self.privateKey = privateKey
|
||||
self.publicKey = publicKey
|
||||
self.npub = npub
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
|
||||
/// Generate a new Nostr identity
|
||||
@@ -39,12 +37,6 @@ struct NostrIdentity: Codable {
|
||||
self.privateKey = privateKeyData
|
||||
self.publicKey = xOnlyPubkey
|
||||
self.npub = try Bech32.encode(hrp: "npub", data: xOnlyPubkey)
|
||||
self.createdAt = Date()
|
||||
}
|
||||
|
||||
/// Get signing key for event signatures
|
||||
func signingKey() throws -> P256K.Signing.PrivateKey {
|
||||
try P256K.Signing.PrivateKey(dataRepresentation: privateKey)
|
||||
}
|
||||
|
||||
/// Get Schnorr signing key for Nostr event signatures
|
||||
|
||||
@@ -37,14 +37,6 @@ final class NostrIdentityBridge {
|
||||
return nostrIdentity
|
||||
}
|
||||
|
||||
/// Associate a Nostr identity with a Noise public key (for favorites)
|
||||
func associateNostrIdentity(_ nostrPubkey: String, with noisePublicKey: Data) {
|
||||
let key = "nostr-noise-\(noisePublicKey.base64EncodedString())"
|
||||
if let data = nostrPubkey.data(using: .utf8) {
|
||||
keychain.save(key: key, data: data, service: keychainService, accessible: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Nostr public key associated with a Noise public key
|
||||
func getNostrPublicKey(for noisePublicKey: Data) -> String? {
|
||||
let key = "nostr-noise-\(noisePublicKey.base64EncodedString())"
|
||||
|
||||
@@ -548,37 +548,6 @@ struct NostrProtocol {
|
||||
return sharedSecretData
|
||||
}
|
||||
|
||||
// Direct version that doesn't try to add prefixes
|
||||
private static func deriveSharedSecretDirect(
|
||||
privateKey: P256K.Schnorr.PrivateKey,
|
||||
publicKey: Data
|
||||
) throws -> Data {
|
||||
// Direct shared secret calculation
|
||||
|
||||
// Convert Schnorr private key to KeyAgreement private key
|
||||
let keyAgreementPrivateKey = try P256K.KeyAgreement.PrivateKey(
|
||||
dataRepresentation: privateKey.dataRepresentation
|
||||
)
|
||||
|
||||
// Use the public key as-is (should already have prefix)
|
||||
let keyAgreementPublicKey = try P256K.KeyAgreement.PublicKey(
|
||||
dataRepresentation: publicKey,
|
||||
format: .compressed
|
||||
)
|
||||
|
||||
// Perform ECDH
|
||||
let sharedSecret = try keyAgreementPrivateKey.sharedSecretFromKeyAgreement(
|
||||
with: keyAgreementPublicKey,
|
||||
format: .compressed
|
||||
)
|
||||
|
||||
// Convert SharedSecret to Data
|
||||
let sharedSecretData = sharedSecret.withUnsafeBytes { Data($0) }
|
||||
|
||||
// Return raw ECDH shared secret; HKDF is applied by deriveNIP44V2Key
|
||||
return sharedSecretData
|
||||
}
|
||||
|
||||
private static func randomizedTimestamp() -> Date {
|
||||
// Add random offset to current time for privacy
|
||||
// This prevents timing correlation attacks while the actual message timestamp
|
||||
@@ -708,11 +677,8 @@ struct NostrEvent: Codable {
|
||||
|
||||
enum NostrError: Error {
|
||||
case invalidPublicKey
|
||||
case invalidPrivateKey
|
||||
case invalidEvent
|
||||
case invalidCiphertext
|
||||
case signingFailed
|
||||
case encryptionFailed
|
||||
}
|
||||
|
||||
// MARK: - NIP-44 v2 helpers (XChaCha20-Poly1305)
|
||||
|
||||
@@ -117,7 +117,6 @@ final class NostrRelayManager: ObservableObject {
|
||||
let url: String
|
||||
var isConnected: Bool = false
|
||||
var lastError: Error?
|
||||
var lastConnectedAt: Date?
|
||||
var messagesSent: Int = 0
|
||||
var messagesReceived: Int = 0
|
||||
var reconnectAttempts: Int = 0
|
||||
@@ -347,7 +346,6 @@ final class NostrRelayManager: ObservableObject {
|
||||
relays[index].nextReconnectTime = nil
|
||||
if resetState {
|
||||
relays[index].lastError = nil
|
||||
relays[index].lastConnectedAt = nil
|
||||
relays[index].lastDisconnectedAt = nil
|
||||
relays[index].messagesSent = 0
|
||||
relays[index].messagesReceived = 0
|
||||
@@ -1075,7 +1073,6 @@ final class NostrRelayManager: ObservableObject {
|
||||
relays[index].isConnected = isConnected
|
||||
relays[index].lastError = error
|
||||
if isConnected {
|
||||
relays[index].lastConnectedAt = dependencies.now()
|
||||
relays[index].reconnectAttempts = 0 // Reset on successful connection
|
||||
relays[index].nextReconnectTime = nil
|
||||
} else {
|
||||
|
||||
@@ -10,14 +10,6 @@ enum Geohash {
|
||||
return map
|
||||
}()
|
||||
|
||||
/// Validates a geohash string for building-level precision (8 characters).
|
||||
/// - Parameter geohash: The geohash string to validate
|
||||
/// - Returns: true if valid 8-character base32 geohash, false otherwise
|
||||
static func isValidBuildingGeohash(_ geohash: String) -> Bool {
|
||||
guard geohash.count == 8 else { return false }
|
||||
return geohash.lowercased().allSatisfy { base32Map[$0] != nil }
|
||||
}
|
||||
|
||||
/// Validates a geohash string at any channel precision (1-12 characters).
|
||||
/// - Parameter geohash: The geohash string to validate
|
||||
/// - Returns: true if a non-empty base32 geohash of at most 12 characters
|
||||
|
||||
@@ -11,14 +11,7 @@ import Foundation
|
||||
/// Manages autocomplete functionality for chat
|
||||
final class AutocompleteService {
|
||||
private let mentionRegex = try? NSRegularExpression(pattern: "@([\\p{L}0-9_]*)$", options: [])
|
||||
private let commandRegex = try? NSRegularExpression(pattern: "^/([a-z]*)$", options: [])
|
||||
|
||||
private let commands = [
|
||||
"/msg", "/who", "/clear",
|
||||
"/hug", "/slap", "/fav", "/unfav",
|
||||
"/block", "/unblock"
|
||||
]
|
||||
|
||||
|
||||
/// Get autocomplete suggestions for current text
|
||||
func getSuggestions(for text: String, peers: [String], cursorPosition: Int) -> (suggestions: [String], range: NSRange?) {
|
||||
let textToPosition = String(text.prefix(cursorPosition))
|
||||
@@ -73,26 +66,6 @@ final class AutocompleteService {
|
||||
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
|
||||
}
|
||||
|
||||
private func getCommandSuggestions(_ text: String) -> ([String], NSRange)? {
|
||||
guard let regex = commandRegex else { return nil }
|
||||
|
||||
let nsText = text as NSString
|
||||
let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsText.length))
|
||||
|
||||
guard let match = matches.last else { return nil }
|
||||
|
||||
let fullRange = match.range(at: 0)
|
||||
let captureRange = match.range(at: 1)
|
||||
let prefix = nsText.substring(with: captureRange).lowercased()
|
||||
|
||||
let suggestions = commands
|
||||
.filter { $0.hasPrefix("/\(prefix)") }
|
||||
.sorted()
|
||||
.prefix(5)
|
||||
|
||||
return suggestions.isEmpty ? nil : (Array(suggestions), fullRange)
|
||||
}
|
||||
|
||||
private func needsArgument(command: String) -> Bool {
|
||||
switch command {
|
||||
case "/who", "/clear":
|
||||
|
||||
@@ -61,7 +61,6 @@ struct BLEFragmentAssemblyBuffer {
|
||||
}
|
||||
|
||||
private struct Metadata {
|
||||
let type: UInt8
|
||||
let total: Int
|
||||
let timestamp: Date
|
||||
let isBroadcast: Bool
|
||||
@@ -150,7 +149,6 @@ struct BLEFragmentAssemblyBuffer {
|
||||
|
||||
fragmentsByKey[header.key] = [:]
|
||||
metadataByKey[header.key] = Metadata(
|
||||
type: header.originalType,
|
||||
total: header.total,
|
||||
timestamp: now,
|
||||
isBroadcast: header.isBroadcastFragment,
|
||||
|
||||
@@ -25,9 +25,4 @@ final class BLELogRateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
queue.sync {
|
||||
lastLogTimeByKey.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ enum BLEOutboundPacketPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
static func priority(for packet: BitchatPacket, data: Data) -> BLEOutboundWritePriority {
|
||||
static func priority(for packet: BitchatPacket, data _: Data) -> BLEOutboundWritePriority {
|
||||
guard let messageType = MessageType(rawValue: packet.type) else { return .low }
|
||||
switch messageType {
|
||||
case .fragment:
|
||||
|
||||
@@ -141,14 +141,6 @@ struct BLEPeerRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
func collisionResolvedNickname(for peerID: PeerID, selfNickname: String) -> String? {
|
||||
guard let info = peers[peerID], info.isVerifiedNickname else { return nil }
|
||||
let hasCollision = peers.values.contains {
|
||||
$0.isConnected && $0.nickname == info.nickname && $0.peerID != peerID
|
||||
} || selfNickname == info.nickname
|
||||
return hasCollision ? info.nickname + "#" + String(peerID.id.prefix(4)) : info.nickname
|
||||
}
|
||||
|
||||
mutating func markDisconnected(_ peerID: PeerID) {
|
||||
guard var info = peers[peerID] else { return }
|
||||
info.isConnected = false
|
||||
|
||||
@@ -10,7 +10,6 @@ import UIKit
|
||||
/// BLEService — Bluetooth Mesh Transport
|
||||
/// - Emits events exclusively via `BitchatDelegate` for UI.
|
||||
/// - ChatViewModel must consume delegate callbacks (`didReceivePublicMessage`, `didReceiveNoisePayload`).
|
||||
/// - A lightweight `peerSnapshotPublisher` is provided for non-UI services.
|
||||
final class BLEService: NSObject {
|
||||
|
||||
// MARK: - Constants
|
||||
@@ -493,13 +492,6 @@ final class BLEService: NSObject {
|
||||
weak var delegate: BitchatDelegate?
|
||||
weak var eventDelegate: TransportEventDelegate?
|
||||
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||
|
||||
// MARK: Peer snapshots publisher (non-UI convenience)
|
||||
|
||||
private let peerSnapshotSubject = PassthroughSubject<[TransportPeerSnapshot], Never>()
|
||||
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
||||
peerSnapshotSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func currentPeerSnapshots() -> [TransportPeerSnapshot] {
|
||||
collectionsQueue.sync {
|
||||
@@ -1323,7 +1315,7 @@ final class BLEService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func handleLeave(_ packet: BitchatPacket, from peerID: PeerID) {
|
||||
private func handleLeave(_: BitchatPacket, from peerID: PeerID) {
|
||||
_ = collectionsQueue.sync(flags: .barrier) {
|
||||
// Remove the peer when they leave
|
||||
peerRegistry.remove(peerID)
|
||||
@@ -2174,7 +2166,7 @@ extension BLEService: CBPeripheralDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func processNotificationPacket(_ packet: BitchatPacket, from peripheral: CBPeripheral, peripheralUUID: String, receivedFrom peerID: PeerID) {
|
||||
private func processNotificationPacket(_ packet: BitchatPacket, from _: CBPeripheral, peripheralUUID: String, receivedFrom peerID: PeerID) {
|
||||
let senderID = PeerID(hexData: packet.senderID)
|
||||
|
||||
if packet.type != MessageType.announce.rawValue {
|
||||
@@ -3422,7 +3414,7 @@ extension BLEService {
|
||||
/// Transport-level handling for a received nostrCarrier packet; policy
|
||||
/// (verification of the carried event, quotas, loop prevention) lives in
|
||||
/// `GatewayService` behind `onNostrCarrierPacket`.
|
||||
private func handleNostrCarrier(_ packet: BitchatPacket, from peerID: PeerID) {
|
||||
private func handleNostrCarrier(_ packet: BitchatPacket, from _: PeerID) {
|
||||
let senderID = PeerID(hexData: packet.senderID)
|
||||
let directedToUs: Bool
|
||||
if let recipientID = packet.recipientID {
|
||||
@@ -4476,7 +4468,7 @@ extension BLEService {
|
||||
/// gossip backfill and hand the payload to the UI layer, where the group
|
||||
/// coordinator decrypts and authenticates against the roster. Non-members
|
||||
/// still relay (generic broadcast relay path) but never decode.
|
||||
private func handleGroupMessage(_ packet: BitchatPacket, from peerID: PeerID) {
|
||||
private func handleGroupMessage(_ packet: BitchatPacket, from _: PeerID) {
|
||||
let isBroadcastRecipient: Bool = {
|
||||
guard let recipient = packet.recipientID else { return true }
|
||||
return recipient.count == 8 && recipient.allSatisfy { $0 == 0xFF }
|
||||
@@ -4636,8 +4628,6 @@ extension BLEService {
|
||||
let transportPeers: [TransportPeerSnapshot] = collectionsQueue.sync {
|
||||
peerRegistry.transportSnapshots(selfNickname: myNickname)
|
||||
}
|
||||
// Notify non-UI listeners
|
||||
peerSnapshotSubject.send(transportPeers)
|
||||
// Notify UI on MainActor via delegate
|
||||
Task { @MainActor [weak self] in
|
||||
self?.peerEventsDelegate?.didUpdatePeerSnapshots(transportPeers)
|
||||
|
||||
@@ -19,7 +19,6 @@ final class BoardManager: ObservableObject {
|
||||
@Published private(set) var posts: [BoardPostPacket] = []
|
||||
|
||||
private let transport: Transport
|
||||
private let store: BoardStore
|
||||
/// Publishes a bridged kind-1 note (expiring with the board post via
|
||||
/// NIP-40) and returns its Nostr event id, or nil when bridging failed or
|
||||
/// was skipped.
|
||||
@@ -39,7 +38,6 @@ final class BoardManager: ObservableObject {
|
||||
deleteFromNostr: ((String, String) -> Void)? = nil
|
||||
) {
|
||||
self.transport = transport
|
||||
self.store = store
|
||||
self.publishToNostr = publishToNostr ?? Self.livePublishToNostr
|
||||
self.deleteFromNostr = deleteFromNostr ?? Self.liveDeleteFromNostr
|
||||
cancellable = store.$postsSnapshot
|
||||
|
||||
@@ -146,14 +146,6 @@ final class BoardStore {
|
||||
|
||||
// MARK: - Maintenance
|
||||
|
||||
func pruneExpired() {
|
||||
let nowMs = currentMs()
|
||||
queue.sync {
|
||||
pruneExpiredLocked(nowMs: nowMs)
|
||||
persistLocked()
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic wipe: drop all board data from memory and disk.
|
||||
func wipe() {
|
||||
queue.sync {
|
||||
|
||||
@@ -272,14 +272,6 @@ final class CourierStore {
|
||||
|
||||
// MARK: - Maintenance
|
||||
|
||||
func pruneExpired() {
|
||||
let date = now()
|
||||
queue.sync {
|
||||
pruneExpiredLocked(at: date)
|
||||
persistLocked()
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic wipe: drop all carried mail from memory and disk.
|
||||
func wipe() {
|
||||
queue.sync {
|
||||
|
||||
@@ -58,12 +58,6 @@ final class StoreAndForwardMetrics {
|
||||
SecureLogger.debug("📊 S&F \(event.rawValue) → \(total)", category: .session)
|
||||
}
|
||||
|
||||
func snapshot() -> [String: Int] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return counts
|
||||
}
|
||||
|
||||
/// Included in the panic wipe alongside the stores it describes.
|
||||
func reset() {
|
||||
lock.lock()
|
||||
|
||||
@@ -82,7 +82,6 @@ final class GatewayService: ObservableObject {
|
||||
let depositor: PeerID
|
||||
let geohash: String
|
||||
let event: NostrEvent
|
||||
let queuedAt: Date
|
||||
}
|
||||
|
||||
static let shared = GatewayService()
|
||||
@@ -221,7 +220,7 @@ final class GatewayService: ObservableObject {
|
||||
publish(event, geohash: carrier.geohash)
|
||||
accepted = true
|
||||
} else {
|
||||
accepted = enqueueUplink(QueuedUplink(depositor: depositor, geohash: carrier.geohash, event: event, queuedAt: now()))
|
||||
accepted = enqueueUplink(QueuedUplink(depositor: depositor, geohash: carrier.geohash, event: event))
|
||||
}
|
||||
|
||||
// Only render on our own timeline what we actually accepted for
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
import Foundation
|
||||
|
||||
/// Represents a participant in a geohash channel
|
||||
public struct GeoPerson: Identifiable, Equatable, Sendable {
|
||||
public let id: String // pubkey hex (lowercased)
|
||||
public let displayName: String
|
||||
public let lastSeen: Date
|
||||
struct GeoPerson: Identifiable, Equatable, Sendable {
|
||||
let id: String // pubkey hex (lowercased)
|
||||
let displayName: String
|
||||
let lastSeen: Date
|
||||
|
||||
public init(id: String, displayName: String, lastSeen: Date) {
|
||||
init(id: String, displayName: String, lastSeen: Date) {
|
||||
self.id = id
|
||||
self.displayName = displayName
|
||||
self.lastSeen = lastSeen
|
||||
@@ -23,7 +23,7 @@ public struct GeoPerson: Identifiable, Equatable, Sendable {
|
||||
|
||||
/// Protocol for resolving display names and checking block status
|
||||
@MainActor
|
||||
public protocol GeohashParticipantContext: AnyObject {
|
||||
protocol GeohashParticipantContext: AnyObject {
|
||||
/// Returns display name for a Nostr pubkey (e.g., "alice#a1b2" or "anon#c3d4")
|
||||
func displayNameForPubkey(_ pubkeyHex: String) -> String
|
||||
/// Returns true if the pubkey is blocked
|
||||
@@ -32,16 +32,16 @@ public protocol GeohashParticipantContext: AnyObject {
|
||||
|
||||
/// Tracks participants across multiple geohash channels
|
||||
@MainActor
|
||||
public final class GeohashParticipantTracker: ObservableObject {
|
||||
final class GeohashParticipantTracker: ObservableObject {
|
||||
|
||||
/// Activity cutoff duration (defaults to 5 minutes)
|
||||
public let activityCutoff: TimeInterval
|
||||
let activityCutoff: TimeInterval
|
||||
|
||||
/// Per-geohash participant map: [geohash: [pubkeyHex: lastSeen]]
|
||||
private var participants: [String: [String: Date]] = [:]
|
||||
|
||||
/// Currently visible people for the active geohash
|
||||
@Published public private(set) var visiblePeople: [GeoPerson] = []
|
||||
@Published private(set) var visiblePeople: [GeoPerson] = []
|
||||
|
||||
/// The currently active geohash (if any)
|
||||
private var activeGeohash: String?
|
||||
@@ -52,17 +52,17 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
/// Timer for periodic refresh
|
||||
private var refreshTimer: Timer?
|
||||
|
||||
public init(activityCutoff: TimeInterval = -300) { // default 5 minutes
|
||||
init(activityCutoff: TimeInterval = -300) { // default 5 minutes
|
||||
self.activityCutoff = activityCutoff
|
||||
}
|
||||
|
||||
/// Configure with a context provider
|
||||
public func configure(context: GeohashParticipantContext) {
|
||||
func configure(context: GeohashParticipantContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// Set the currently active geohash
|
||||
public func setActiveGeohash(_ geohash: String?) {
|
||||
func setActiveGeohash(_ geohash: String?) {
|
||||
activeGeohash = geohash
|
||||
if geohash == nil {
|
||||
visiblePeople = []
|
||||
@@ -72,13 +72,13 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
}
|
||||
|
||||
/// Record activity from a participant in the current active geohash
|
||||
public func recordParticipant(pubkeyHex: String) {
|
||||
func recordParticipant(pubkeyHex: String) {
|
||||
guard let gh = activeGeohash else { return }
|
||||
recordParticipant(pubkeyHex: pubkeyHex, geohash: gh)
|
||||
}
|
||||
|
||||
/// Record activity from a participant in a specific geohash
|
||||
public func recordParticipant(pubkeyHex: String, geohash: String) {
|
||||
func recordParticipant(pubkeyHex: String, geohash: String) {
|
||||
let key = pubkeyHex.lowercased()
|
||||
var map = participants[geohash] ?? [:]
|
||||
map[key] = Date()
|
||||
@@ -94,7 +94,7 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
}
|
||||
|
||||
/// Remove a participant from all geohashes (used when blocking)
|
||||
public func removeParticipant(pubkeyHex: String) {
|
||||
func removeParticipant(pubkeyHex: String) {
|
||||
let key = pubkeyHex.lowercased()
|
||||
for (gh, var map) in participants {
|
||||
map.removeValue(forKey: key)
|
||||
@@ -104,14 +104,14 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
}
|
||||
|
||||
/// Get participant count for a specific geohash
|
||||
public func participantCount(for geohash: String) -> Int {
|
||||
func participantCount(for geohash: String) -> Int {
|
||||
let cutoff = Date().addingTimeInterval(activityCutoff)
|
||||
let map = participants[geohash] ?? [:]
|
||||
return map.values.filter { $0 >= cutoff }.count
|
||||
}
|
||||
|
||||
/// Get the visible people list for the active geohash (read-only query)
|
||||
public func getVisiblePeople() -> [GeoPerson] {
|
||||
func getVisiblePeople() -> [GeoPerson] {
|
||||
guard let gh = activeGeohash, let context = context else { return [] }
|
||||
let cutoff = Date().addingTimeInterval(activityCutoff)
|
||||
let map = (participants[gh] ?? [:])
|
||||
@@ -126,12 +126,12 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
}
|
||||
|
||||
/// Refresh the visible people list
|
||||
public func refresh() {
|
||||
func refresh() {
|
||||
visiblePeople = getVisiblePeople()
|
||||
}
|
||||
|
||||
/// Start the periodic refresh timer
|
||||
public func startRefreshTimer(interval: TimeInterval = 30.0) {
|
||||
func startRefreshTimer(interval: TimeInterval = 30.0) {
|
||||
stopRefreshTimer()
|
||||
refreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
@@ -141,19 +141,19 @@ public final class GeohashParticipantTracker: ObservableObject {
|
||||
}
|
||||
|
||||
/// Stop the periodic refresh timer
|
||||
public func stopRefreshTimer() {
|
||||
func stopRefreshTimer() {
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = nil
|
||||
}
|
||||
|
||||
/// Clear all participant data
|
||||
public func clear() {
|
||||
func clear() {
|
||||
participants.removeAll()
|
||||
visiblePeople = []
|
||||
}
|
||||
|
||||
/// Clear participant data for a specific geohash
|
||||
public func clear(geohash: String) {
|
||||
func clear(geohash: String) {
|
||||
participants.removeValue(forKey: geohash)
|
||||
if activeGeohash == geohash {
|
||||
visiblePeople = []
|
||||
|
||||
@@ -406,7 +406,6 @@ enum GroupCryptoError: Error, Equatable {
|
||||
case malformedPayload
|
||||
case signingFailed
|
||||
case sealFailed
|
||||
case wrongEpoch
|
||||
case decryptionFailed
|
||||
case badSenderSignature
|
||||
}
|
||||
|
||||
@@ -255,11 +255,6 @@ final class KeychainManager: KeychainManagerProtocol {
|
||||
|
||||
// MARK: - Generic Operations
|
||||
|
||||
private func save(_ value: String, forKey key: String) -> Bool {
|
||||
guard let data = value.data(using: .utf8) else { return false }
|
||||
return saveData(data, forKey: key)
|
||||
}
|
||||
|
||||
private func saveData(_ data: Data, forKey key: String) -> Bool {
|
||||
// Delete any existing item first to ensure clean state
|
||||
_ = delete(forKey: key)
|
||||
@@ -305,11 +300,6 @@ final class KeychainManager: KeychainManagerProtocol {
|
||||
return false
|
||||
}
|
||||
|
||||
private func retrieve(forKey key: String) -> String? {
|
||||
guard let data = retrieveData(forKey: key) else { return nil }
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func retrieveData(forKey key: String) -> Data? {
|
||||
// Base query
|
||||
let base: [String: Any] = [
|
||||
@@ -365,22 +355,7 @@ final class KeychainManager: KeychainManagerProtocol {
|
||||
}
|
||||
|
||||
// MARK: - Cleanup
|
||||
|
||||
func deleteAllPasswords() -> Bool {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword
|
||||
]
|
||||
|
||||
// Add service if not empty
|
||||
if !service.isEmpty {
|
||||
query[kSecAttrService as String] = service
|
||||
}
|
||||
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
return status == errSecSuccess || status == errSecItemNotFound
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Delete ALL keychain data for panic mode
|
||||
func deleteAllKeychainData() -> Bool {
|
||||
SecureLogger.warning("Panic mode - deleting all keychain data", category: .security)
|
||||
|
||||
@@ -101,9 +101,7 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
|
||||
|
||||
private let cl: LocationStateManaging
|
||||
private let geocoder: LocationStateGeocoding
|
||||
private var lastLocation: CLLocation?
|
||||
private var refreshTimer: Timer?
|
||||
private var isGeocoding: Bool = false
|
||||
|
||||
// MARK: - Persistence Keys
|
||||
|
||||
@@ -268,7 +266,7 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
|
||||
}
|
||||
}
|
||||
|
||||
func beginLiveRefresh(interval: TimeInterval = TransportConfig.locationLiveRefreshInterval) {
|
||||
func beginLiveRefresh(interval _: TimeInterval = TransportConfig.locationLiveRefreshInterval) {
|
||||
guard permissionState == .authorized else { return }
|
||||
refreshTimer?.invalidate()
|
||||
refreshTimer = nil
|
||||
@@ -385,7 +383,6 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
|
||||
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let loc = locations.last else { return }
|
||||
lastLocation = loc
|
||||
computeChannels(from: loc.coordinate)
|
||||
reverseGeocodeLocation(loc)
|
||||
}
|
||||
@@ -441,10 +438,8 @@ final class LocationStateManager: NSObject, CLLocationManagerDelegate, Observabl
|
||||
|
||||
private func reverseGeocodeLocation(_ location: CLLocation) {
|
||||
geocoder.cancelGeocode()
|
||||
isGeocoding = true
|
||||
geocoder.reverseGeocodeLocation(location) { [weak self] placemarks, _ in
|
||||
guard let self = self else { return }
|
||||
self.isGeocoding = false
|
||||
if let pm = placemarks?.first {
|
||||
let names = self.locationNamesByLevel(from: pm)
|
||||
Task { @MainActor in self.locationNames = names }
|
||||
@@ -632,15 +627,5 @@ extension LocationStateManager {
|
||||
func toggle(_ geohash: String) {
|
||||
toggleBookmark(geohash)
|
||||
}
|
||||
|
||||
/// Backward compatibility: add bookmark (was GeohashBookmarksStore.add)
|
||||
func add(_ geohash: String) {
|
||||
addBookmark(geohash)
|
||||
}
|
||||
|
||||
/// Backward compatibility: remove bookmark (was GeohashBookmarksStore.remove)
|
||||
func remove(_ geohash: String) {
|
||||
removeBookmark(geohash)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -70,10 +70,6 @@ final class MessageFormattingEngine {
|
||||
static let quickCashuPresence: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "\\bcashu[AB][A-Za-z0-9._-]{40,}\\b", options: [])
|
||||
}()
|
||||
|
||||
static let simplifyHTTPURL: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "https?://[^\\s?#]+(?:[?#][^\\s]*)?", options: [.caseInsensitive])
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Match Types
|
||||
@@ -195,7 +191,7 @@ final class MessageFormattingEngine {
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
private static func formatSystemMessage(_ message: BitchatMessage, isDark: Bool) -> AttributedString {
|
||||
private static func formatSystemMessage(_ message: BitchatMessage, isDark _: Bool) -> AttributedString {
|
||||
var result = AttributedString()
|
||||
|
||||
let content = AttributedString("* \(message.content) *")
|
||||
@@ -414,7 +410,7 @@ final class MessageFormattingEngine {
|
||||
return AttributedString(text).mergingAttributes(style)
|
||||
}
|
||||
|
||||
private static func formatMatch(_ text: String, type: MatchType, baseColor: Color, isSelf: Bool) -> AttributedString {
|
||||
private static func formatMatch(_ text: String, type: MatchType, baseColor _: Color, isSelf _: Bool) -> AttributedString {
|
||||
var style = AttributeContainer()
|
||||
|
||||
switch type {
|
||||
|
||||
@@ -311,13 +311,6 @@ final class MessageRouter {
|
||||
}
|
||||
}
|
||||
|
||||
func sendDeliveryAck(_ messageID: String, to peerID: PeerID) {
|
||||
if let transport = reachableTransport(for: peerID) {
|
||||
SecureLogger.debug("Routing DELIVERED ack via \(type(of: transport)) to \(peerID.id.prefix(8))… id=\(messageID.prefix(8))…", category: .session)
|
||||
transport.sendDeliveryAck(for: messageID, to: peerID)
|
||||
}
|
||||
}
|
||||
|
||||
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||
if let transport = connectedTransport(for: peerID) {
|
||||
transport.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||
|
||||
@@ -153,11 +153,11 @@ enum EncryptionStatus: Equatable {
|
||||
final class NoiseEncryptionService {
|
||||
// Static identity key (persistent across sessions)
|
||||
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
|
||||
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
|
||||
let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
|
||||
|
||||
// Ed25519 signing key (persistent across sessions)
|
||||
private let signingKey: Curve25519.Signing.PrivateKey
|
||||
public let signingPublicKey: Curve25519.Signing.PublicKey
|
||||
let signingPublicKey: Curve25519.Signing.PublicKey
|
||||
|
||||
// Session manager
|
||||
private let sessionManager: NoiseSessionManager
|
||||
|
||||
@@ -13,7 +13,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
let currentIdentity: @MainActor () throws -> NostrIdentity?
|
||||
let registerPendingGiftWrap: @MainActor (String) -> Void
|
||||
let sendEvent: @MainActor (NostrEvent) -> Void
|
||||
let scheduleAfter: @Sendable (TimeInterval, @escaping @Sendable () -> Void) -> Void
|
||||
/// Emits whether a relay that carries private messages is up
|
||||
/// (fail-closed behind Tor). A connected geohash/custom relay alone
|
||||
/// doesn't count: DM sends target the default relay set and would
|
||||
@@ -42,7 +41,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
self.currentIdentity = currentIdentity
|
||||
self.registerPendingGiftWrap = registerPendingGiftWrap
|
||||
self.sendEvent = sendEvent
|
||||
self.scheduleAfter = scheduleAfter
|
||||
self.relayConnectivity = relayConnectivity
|
||||
// Default pacer drives its throttle through the same injected
|
||||
// scheduler, so tests that step scheduleAfter manually keep
|
||||
@@ -130,8 +128,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
static let sharedAckPacer = AckPacer()
|
||||
private let keychain: KeychainManagerProtocol
|
||||
private let idBridge: NostrIdentityBridge
|
||||
private let dependencies: Dependencies
|
||||
private var favoriteStatusObserver: NSObjectProtocol?
|
||||
|
||||
@@ -145,12 +141,10 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
|
||||
@MainActor
|
||||
init(
|
||||
keychain: KeychainManagerProtocol,
|
||||
keychain _: KeychainManagerProtocol,
|
||||
idBridge: NostrIdentityBridge,
|
||||
dependencies: Dependencies? = nil
|
||||
) {
|
||||
self.keychain = keychain
|
||||
self.idBridge = idBridge
|
||||
self.dependencies = dependencies ?? .live(idBridge: idBridge)
|
||||
|
||||
setupObservers()
|
||||
@@ -207,9 +201,6 @@ final class NostrTransport: Transport, @unchecked Sendable {
|
||||
weak var eventDelegate: TransportEventDelegate?
|
||||
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||
|
||||
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
||||
Just([]).eraseToAnyPublisher()
|
||||
}
|
||||
func currentPeerSnapshots() -> [TransportPeerSnapshot] { [] }
|
||||
|
||||
var myPeerID: PeerID { senderPeerID }
|
||||
|
||||
@@ -28,7 +28,6 @@ final class PrivateChatManager: ObservableObject {
|
||||
@Published private(set) var selectedPeer: PeerID? = nil
|
||||
private var selectedPeerMirrorCancellable: AnyCancellable? = nil
|
||||
|
||||
private var selectedPeerFingerprint: String? = nil
|
||||
var sentReadReceipts: Set<String> = [] // Made accessible for ChatViewModel
|
||||
|
||||
weak var meshService: Transport?
|
||||
@@ -219,18 +218,13 @@ final class PrivateChatManager: ObservableObject {
|
||||
|
||||
/// Start a private chat with a peer. Selection is mutated through the
|
||||
/// store's intent (the store owns it); the manager keeps its side
|
||||
/// effects (fingerprint tracking, read receipts, unread clearing).
|
||||
/// effects (read receipts, unread clearing).
|
||||
@MainActor
|
||||
func startChat(with peerID: PeerID) {
|
||||
// Also creates the conversation if needed and updates the derived
|
||||
// `selectedConversationID`; `selectedPeer` mirrors the change.
|
||||
conversationStore?.setSelectedPrivatePeer(peerID)
|
||||
|
||||
// Store fingerprint for persistence across reconnections
|
||||
if let fingerprint = meshService?.getFingerprint(for: peerID) {
|
||||
selectedPeerFingerprint = fingerprint
|
||||
}
|
||||
|
||||
// Mark messages as read
|
||||
markAsRead(from: peerID)
|
||||
}
|
||||
@@ -239,15 +233,8 @@ final class PrivateChatManager: ObservableObject {
|
||||
/// channel's conversation).
|
||||
func endChat() {
|
||||
conversationStore?.setSelectedPrivatePeer(nil)
|
||||
selectedPeerFingerprint = nil
|
||||
}
|
||||
|
||||
/// No-op since the `ConversationStore` cutover: the store maintains
|
||||
/// chronological order and dedups by message ID on every insert, so the
|
||||
/// per-append re-sort/dedup sweep this performed is no longer needed.
|
||||
/// Kept only for API compatibility until step 5 removes the callers.
|
||||
func sanitizeChat(for peerID: PeerID) {}
|
||||
|
||||
/// Mark messages from a peer as read
|
||||
@MainActor
|
||||
func markAsRead(from peerID: PeerID) {
|
||||
|
||||
@@ -12,7 +12,7 @@ struct RelayController {
|
||||
static func decide(ttl: UInt8,
|
||||
senderIsSelf: Bool,
|
||||
recipientIsSelf: Bool = false,
|
||||
isEncrypted: Bool,
|
||||
isEncrypted _: Bool,
|
||||
isDirectedEncrypted: Bool,
|
||||
isFragment: Bool,
|
||||
isDirectedFragment: Bool,
|
||||
|
||||
@@ -49,12 +49,6 @@ final class TransferProgressManager {
|
||||
}
|
||||
}
|
||||
|
||||
func reset(id: String) {
|
||||
queue.async(flags: .barrier) { [weak self] in
|
||||
self?.states.removeValue(forKey: id)
|
||||
}
|
||||
}
|
||||
|
||||
func snapshot(id: String) -> (sent: Int, total: Int)? {
|
||||
var result: (sent: Int, total: Int)?
|
||||
queue.sync {
|
||||
|
||||
@@ -96,7 +96,6 @@ protocol Transport: AnyObject {
|
||||
var peerEventsDelegate: TransportPeerEventsDelegate? { get set }
|
||||
|
||||
// Peer snapshots (for non-UI services)
|
||||
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> { get }
|
||||
func currentPeerSnapshots() -> [TransportPeerSnapshot]
|
||||
|
||||
// Identity
|
||||
@@ -265,7 +264,7 @@ extension Transport {
|
||||
}
|
||||
|
||||
protocol TransportPeerEventsDelegate: AnyObject {
|
||||
@MainActor func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot])
|
||||
@MainActor func didUpdatePeerSnapshots(_: [TransportPeerSnapshot])
|
||||
}
|
||||
|
||||
extension BitchatDelegate {
|
||||
|
||||
@@ -27,7 +27,6 @@ enum TransportConfig {
|
||||
static let pttJitterBufferSeconds: TimeInterval = 0.35 // buffered audio before live playback starts
|
||||
static let pttJitterDeadlineSeconds: TimeInterval = 0.5 // start anyway after this wall-clock wait
|
||||
static let pttBurstEndTimeoutSeconds: TimeInterval = 3.0 // no frames -> burst considered ended
|
||||
static let pttAssemblyStaleSeconds: TimeInterval = 30.0 // drop half-open assemblies after this
|
||||
static let pttMaxConcurrentAssemblies: Int = 8 // concurrent inbound bursts cap
|
||||
static let pttMaxBurstBytes: Int = 384 * 1024 // 120s at ~2KB/s + generous slack
|
||||
static let pttFinishedBurstRegistrySeconds: TimeInterval = 600 // window to absorb the finalized note
|
||||
@@ -93,8 +92,7 @@ enum TransportConfig {
|
||||
|
||||
// UI thresholds
|
||||
static let uiProcessedNostrEventsCap: Int = 2000
|
||||
static let uiChannelInactivityThresholdSeconds: TimeInterval = 9 * 60
|
||||
|
||||
|
||||
// UI rate limiters (token buckets)
|
||||
static let uiSenderRateBucketCapacity: Double = 5
|
||||
static let uiSenderRateBucketRefillPerSec: Double = 1.0
|
||||
@@ -103,17 +101,13 @@ enum TransportConfig {
|
||||
|
||||
// UI sleeps/delays
|
||||
static let uiStartupInitialDelaySeconds: TimeInterval = 1.0
|
||||
static let uiStartupShortSleepNs: UInt64 = 200_000_000
|
||||
static let uiStartupPhaseDurationSeconds: TimeInterval = 2.0
|
||||
static let uiAsyncShortSleepNs: UInt64 = 100_000_000
|
||||
static let uiAsyncMediumSleepNs: UInt64 = 500_000_000
|
||||
static let uiReadReceiptRetryShortSeconds: TimeInterval = 0.1
|
||||
static let uiReadReceiptRetryLongSeconds: TimeInterval = 0.5
|
||||
static let uiBatchDispatchStaggerSeconds: TimeInterval = 0.15
|
||||
static let uiScrollThrottleSeconds: TimeInterval = 0.5
|
||||
static let uiAnimationShortSeconds: TimeInterval = 0.15
|
||||
static let uiAnimationMediumSeconds: TimeInterval = 0.2
|
||||
static let uiAnimationSidebarSeconds: TimeInterval = 0.25
|
||||
static let uiRecentCutoffFiveMinutesSeconds: TimeInterval = 5 * 60
|
||||
static let uiMeshEmptyConfirmationSeconds: TimeInterval = 30.0
|
||||
|
||||
@@ -179,10 +173,6 @@ enum TransportConfig {
|
||||
static let nostrGeohashSampleLimit: Int = 100
|
||||
static let nostrDMSubscribeLookbackSeconds: TimeInterval = 86400
|
||||
|
||||
// Nostr helpers
|
||||
static let nostrShortKeyDisplayLength: Int = 8
|
||||
static let nostrConvKeyPrefixLength: Int = 16
|
||||
|
||||
// Message deduplication
|
||||
static let messageDedupMaxAgeSeconds: TimeInterval = 300
|
||||
static let messageDedupMaxCount: Int = 1000
|
||||
@@ -294,12 +284,7 @@ enum TransportConfig {
|
||||
static let uiVeryLongTokenThreshold: Int = 512
|
||||
static let uiLongMessageLineLimit: Int = 30
|
||||
static let uiFingerprintSampleCount: Int = 3
|
||||
|
||||
// UI swipe/gesture thresholds
|
||||
static let uiBackSwipeTranslationLarge: CGFloat = 50
|
||||
static let uiBackSwipeTranslationSmall: CGFloat = 30
|
||||
static let uiBackSwipeVelocityThreshold: CGFloat = 300
|
||||
|
||||
|
||||
// UI color tuning
|
||||
static let uiColorHueAvoidanceDelta: Double = 0.05
|
||||
static let uiColorHueOffset: Double = 0.12
|
||||
|
||||
@@ -70,7 +70,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
}
|
||||
|
||||
// TransportPeerEventsDelegate
|
||||
func didUpdatePeerSnapshots(_ peers: [TransportPeerSnapshot]) {
|
||||
func didUpdatePeerSnapshots(_: [TransportPeerSnapshot]) {
|
||||
updatePeers()
|
||||
}
|
||||
|
||||
@@ -367,12 +367,7 @@ final class UnifiedPeerService: ObservableObject, TransportPeerEventsDelegate {
|
||||
}
|
||||
|
||||
// MARK: - Compatibility Methods (for easy migration)
|
||||
|
||||
var allPeers: [BitchatPeer] { peers }
|
||||
var connectedPeers: Set<PeerID> { connectedPeerIDs }
|
||||
var favoritePeers: Set<String> {
|
||||
Set(favorites.compactMap { getFingerprint(for: $0.peerID) })
|
||||
}
|
||||
|
||||
var blockedUsers: Set<String> {
|
||||
Set(peers.compactMap { peer in
|
||||
isBlocked(peer.peerID) ? getFingerprint(for: peer.peerID) : nil
|
||||
|
||||
@@ -52,18 +52,6 @@ final class MessageDeduplicator {
|
||||
return false
|
||||
}
|
||||
|
||||
/// Record an ID with a specific timestamp (for content key tracking)
|
||||
func record(_ id: String, timestamp: Date) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
if lookup[id] == nil {
|
||||
entries.append(Entry(id: id, timestamp: timestamp))
|
||||
}
|
||||
lookup[id] = timestamp
|
||||
trimIfNeeded()
|
||||
}
|
||||
|
||||
/// Add an ID without checking (for announce-back tracking)
|
||||
func markProcessed(_ id: String) {
|
||||
lock.lock()
|
||||
@@ -83,13 +71,6 @@ final class MessageDeduplicator {
|
||||
return lookup[id] != nil
|
||||
}
|
||||
|
||||
/// Get timestamp for an ID (for content deduplication time-window checks)
|
||||
func timestampFor(_ id: String) -> Date? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return lookup[id]
|
||||
}
|
||||
|
||||
private func trimIfNeeded() {
|
||||
let activeCount = entries.count - head
|
||||
guard activeCount > maxCount else { return }
|
||||
|
||||
@@ -96,7 +96,7 @@ struct ThemePalette {
|
||||
)
|
||||
}
|
||||
|
||||
static func liquidGlass(_ colorScheme: ColorScheme) -> ThemePalette {
|
||||
static func liquidGlass(_: ColorScheme) -> ThemePalette {
|
||||
ThemePalette(
|
||||
background: systemBackground,
|
||||
primary: .primary,
|
||||
|
||||
@@ -387,7 +387,7 @@ final class ChatGroupCoordinator {
|
||||
/// Decrypt-verify path for an incoming 0x25 broadcast. Drops silently for
|
||||
/// unknown groups (non-members relay but never read), wrong epochs, bad
|
||||
/// sender signatures, and senders missing from the pinned roster.
|
||||
func handleGroupMessagePayload(_ payload: Data, timestamp: Date) {
|
||||
func handleGroupMessagePayload(_ payload: Data, timestamp _: Date) {
|
||||
guard let envelope = GroupMessageEnvelope.decode(payload) else { return }
|
||||
guard let group = context.groupStore.group(withID: envelope.groupID) else { return }
|
||||
guard envelope.epoch == group.epoch else {
|
||||
|
||||
@@ -29,7 +29,6 @@ protocol ChatPeerIdentityContext: AnyObject {
|
||||
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
|
||||
var selectedPrivateChatPeer: PeerID? { get set }
|
||||
var selectedPrivateChatFingerprint: String? { get set }
|
||||
var nickname: String { get }
|
||||
var myPeerID: PeerID { get }
|
||||
var activeChannel: ChannelID { get }
|
||||
/// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`).
|
||||
@@ -104,7 +103,7 @@ protocol ChatPeerIdentityContext: AnyObject {
|
||||
|
||||
extension ChatViewModel: ChatPeerIdentityContext {
|
||||
// `privateChats`, `unreadPrivateMessages`, `selectedPrivateChatPeer`,
|
||||
// `selectedPrivateChatFingerprint`, `nickname`, `myPeerID`,
|
||||
// `selectedPrivateChatFingerprint`, `myPeerID`,
|
||||
// `activeChannel`, `connectedPeers`, `geoNicknames`, `notifyUIChanged()`,
|
||||
// `addSystemMessage(_:)`, `peerNickname(for:)`, `meshPeerNicknames()`,
|
||||
// `ephemeralPeerID(forNoiseKey:)`, `unifiedPeer(for:)`,
|
||||
|
||||
@@ -19,7 +19,6 @@ protocol ChatPrivateConversationContext: AnyObject {
|
||||
/// lookup on `ChatViewModel` (no `privateChats` dictionary build).
|
||||
func privateMessages(for peerID: PeerID) -> [BitchatMessage]
|
||||
var sentReadReceipts: Set<String> { get }
|
||||
var unreadPrivateMessages: Set<PeerID> { get }
|
||||
var selectedPrivateChatPeer: PeerID? { get }
|
||||
var nickname: String { get }
|
||||
var activeChannel: ChannelID { get }
|
||||
@@ -40,8 +39,6 @@ protocol ChatPrivateConversationContext: AnyObject {
|
||||
func setPrivateDeliveryStatus(_ status: DeliveryStatus, forMessageID messageID: String, peerID: PeerID) -> Bool
|
||||
func markPrivateChatUnread(_ peerID: PeerID)
|
||||
func markPrivateChatRead(_ peerID: PeerID)
|
||||
/// Removes the peer's chat entirely, including unread state.
|
||||
func removePrivateChat(_ peerID: PeerID)
|
||||
/// Moves all messages from `oldPeerID`'s chat into `newPeerID`'s chat
|
||||
/// (dedup by ID, order preserved, unread carried, old chat removed).
|
||||
func migratePrivateChat(from oldPeerID: PeerID, to newPeerID: PeerID)
|
||||
@@ -87,7 +84,6 @@ protocol ChatPrivateConversationContext: AnyObject {
|
||||
// MARK: Routing & acknowledgements
|
||||
func routePrivateMessage(_ content: String, to peerID: PeerID, recipientNickname: String, messageID: String)
|
||||
func routeReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
|
||||
func routeFavoriteNotification(to peerID: PeerID, isFavorite: Bool)
|
||||
func sendMeshReadReceipt(_ receipt: ReadReceipt, to peerID: PeerID)
|
||||
func sendGeohashPrivateMessage(_ content: String, toRecipientHex recipientHex: String, from identity: NostrIdentity, messageID: String)
|
||||
func sendGeohashDeliveryAck(for messageID: String, toRecipientHex recipientHex: String, from identity: NostrIdentity)
|
||||
@@ -606,7 +602,7 @@ final class ChatPrivateConversationCoordinator {
|
||||
|
||||
/// O(1)-per-conversation dedup via the store's message-ID indexes
|
||||
/// (replaces the full scan over every private chat).
|
||||
func isDuplicateMessage(_ messageId: String, targetPeerID: PeerID) -> Bool {
|
||||
func isDuplicateMessage(_ messageId: String, targetPeerID _: PeerID) -> Bool {
|
||||
context.privateChatsContainMessage(withID: messageId)
|
||||
}
|
||||
|
||||
@@ -825,25 +821,6 @@ final class ChatPrivateConversationCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||
var noiseKey: Data?
|
||||
|
||||
if let hexKey = Data(hexString: peerID.id) {
|
||||
noiseKey = hexKey
|
||||
} else if let peerNoiseKey = context.noisePublicKey(for: peerID) {
|
||||
noiseKey = peerNoiseKey
|
||||
}
|
||||
|
||||
if context.isPeerConnected(peerID) {
|
||||
context.routeFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||
SecureLogger.debug("📤 Sent favorite notification via BLE to \(peerID)", category: .session)
|
||||
} else if let key = noiseKey {
|
||||
context.routeFavoriteNotification(to: PeerID(hexData: key), isFavorite: isFavorite)
|
||||
} else {
|
||||
SecureLogger.warning("⚠️ Cannot send favorite notification - peer not connected and no Nostr pubkey", category: .session)
|
||||
}
|
||||
}
|
||||
|
||||
func isMessageBlocked(_ message: BitchatMessage) -> Bool {
|
||||
if let peerID = message.senderPeerID ?? context.getPeerIDForNickname(message.sender) {
|
||||
if context.isPeerBlocked(peerID) { return true }
|
||||
|
||||
@@ -36,9 +36,6 @@ protocol ChatPublicConversationContext: AnyObject {
|
||||
/// message with the same ID is already in that conversation.
|
||||
@discardableResult
|
||||
func appendPublicMessage(_ message: BitchatMessage, to conversationID: ConversationID) -> Bool
|
||||
/// Appends a geohash message if absent. Returns `true` when stored.
|
||||
@discardableResult
|
||||
func appendGeohashMessageIfAbsent(_ message: BitchatMessage, toGeohash geohash: String) -> Bool
|
||||
func publicConversationContainsMessage(withID messageID: String, in conversationID: ConversationID) -> Bool
|
||||
/// Removes a message by ID from whichever public conversation contains it.
|
||||
@discardableResult
|
||||
@@ -109,7 +106,6 @@ extension ChatViewModel: ChatPublicConversationContext {
|
||||
// `geoParticipantCount(for:)`, `isNostrBlocked(pubkeyHexLowercased:)`,
|
||||
// `deriveNostrIdentity(forGeohash:)`, the public conversation store
|
||||
// intents (`appendPublicMessage(_:to:)`,
|
||||
// `appendGeohashMessageIfAbsent(_:toGeohash:)`,
|
||||
// `publicConversationContainsMessage(withID:in:)`,
|
||||
// `removePublicMessage(withID:)`,
|
||||
// `removePublicMessages(fromGeohash:where:)`,
|
||||
@@ -520,27 +516,27 @@ final class ChatPublicConversationCoordinator: PublicMessagePipelineDelegate {
|
||||
#endif
|
||||
}
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String {
|
||||
func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String {
|
||||
context.normalizedContentKey(content)
|
||||
}
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
|
||||
func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date? {
|
||||
context.contentTimestamp(forKey: key)
|
||||
}
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
|
||||
func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date) {
|
||||
context.recordContentKey(key, timestamp: timestamp)
|
||||
}
|
||||
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
|
||||
func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool {
|
||||
context.appendPublicMessage(message, to: conversationID)
|
||||
}
|
||||
|
||||
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage) {
|
||||
func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage) {
|
||||
context.prewarmMessageFormatting(message)
|
||||
}
|
||||
|
||||
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool) {
|
||||
func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool) {
|
||||
context.setPublicBatching(isBatching)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,6 @@ final class ChatVerificationCoordinator {
|
||||
let noiseKeyHex: String
|
||||
let signKeyHex: String
|
||||
let nonceA: Data
|
||||
let startedAt: Date
|
||||
var sent: Bool
|
||||
}
|
||||
|
||||
@@ -258,7 +257,6 @@ final class ChatVerificationCoordinator {
|
||||
noiseKeyHex: qr.noiseKeyHex,
|
||||
signKeyHex: qr.signKeyHex,
|
||||
nonceA: nonce,
|
||||
startedAt: Date(),
|
||||
sent: false
|
||||
)
|
||||
pendingQRVerifications[peerID] = pending
|
||||
|
||||
@@ -84,7 +84,6 @@ import SwiftUI
|
||||
import Combine
|
||||
import CommonCrypto
|
||||
import CoreBluetooth
|
||||
import Tor
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
@@ -222,12 +221,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
conversations.unreadDirectRoutingPeerIDs()
|
||||
}
|
||||
|
||||
/// Check if there are any unread messages (including from temporary Nostr peer IDs)
|
||||
@MainActor
|
||||
var hasAnyUnreadMessages: Bool {
|
||||
!unreadPrivateMessages.isEmpty
|
||||
}
|
||||
|
||||
/// Open the most relevant private chat when tapping the toolbar unread icon.
|
||||
/// Prefers the most recently active unread conversation, otherwise the most recent PM.
|
||||
@MainActor
|
||||
@@ -245,20 +238,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
set { peerIdentityStore.setSelectedPrivateChatFingerprint(newValue) }
|
||||
}
|
||||
|
||||
// Resolve full Noise key for a peer's short ID (used by UI header rendering)
|
||||
@MainActor
|
||||
private func getNoiseKeyForShortID(_ shortPeerID: PeerID) -> PeerID? {
|
||||
if let mapped = peerIdentityStore.stablePeerID(forShortID: shortPeerID) { return mapped }
|
||||
// Fallback: derive from active Noise session if available
|
||||
if shortPeerID.id.count == 16,
|
||||
let key = meshService.noiseSessionPublicKeyData(for: shortPeerID) {
|
||||
let stable = PeerID(hexData: key)
|
||||
peerIdentityStore.setStablePeerID(stable, forShortID: shortPeerID)
|
||||
return stable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve short mesh ID (16-hex) from a full Noise public key hex (64-hex)
|
||||
@MainActor
|
||||
func getShortIDForNoiseKey(_ fullNoiseKeyHex: PeerID) -> PeerID {
|
||||
@@ -354,18 +333,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
|
||||
// MARK: - Social Features (Delegated to PeerStateManager)
|
||||
|
||||
@MainActor
|
||||
var favoritePeers: Set<String> { unifiedPeerService.favoritePeers }
|
||||
@MainActor
|
||||
var blockedUsers: Set<String> { unifiedPeerService.blockedUsers }
|
||||
|
||||
// MARK: - Encryption and Security
|
||||
|
||||
// Noise Protocol encryption status
|
||||
var peerEncryptionStatus: [PeerID: EncryptionStatus] {
|
||||
get { peerIdentityStore.encryptionStatuses }
|
||||
set { peerIdentityStore.replaceEncryptionStatuses(newValue) }
|
||||
}
|
||||
var verifiedFingerprints: Set<String> {
|
||||
get { peerIdentityStore.verifiedFingerprints }
|
||||
set { peerIdentityStore.setVerifiedFingerprints(newValue) }
|
||||
@@ -959,11 +931,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
|
||||
// MARK: - Public helpers
|
||||
|
||||
/// Published geohash people list for SwiftUI observation
|
||||
var geohashPeople: [GeoPerson] {
|
||||
participantTracker.visiblePeople
|
||||
}
|
||||
|
||||
/// Return the current, pruned, sorted people list for the active geohash without mutating state.
|
||||
@MainActor
|
||||
func visibleGeohashPeople() -> [GeoPerson] {
|
||||
@@ -1056,14 +1023,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
publicConversationCoordinator.displayNameForNostrPubkey(pubkeyHex)
|
||||
}
|
||||
|
||||
// MARK: - Media Transfers
|
||||
|
||||
private enum MediaSendError: Error {
|
||||
case encodingFailed
|
||||
case tooLarge
|
||||
case copyFailed
|
||||
}
|
||||
|
||||
func currentPublicSender() -> (name: String, peerID: PeerID) {
|
||||
publicConversationCoordinator.currentPublicSender()
|
||||
}
|
||||
@@ -1125,7 +1084,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@objc func handlePeerStatusUpdate(_ notification: Notification) {
|
||||
@objc func handlePeerStatusUpdate(_: Notification) {
|
||||
peerIdentityCoordinator.handlePeerStatusUpdate()
|
||||
}
|
||||
|
||||
@@ -1159,15 +1118,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, TransportEventDele
|
||||
lifecycleCoordinator.markPrivateMessagesAsRead(from: peerID)
|
||||
}
|
||||
|
||||
func getMessages(for peerID: PeerID?) -> [BitchatMessage] {
|
||||
lifecycleCoordinator.getMessages(for: peerID)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func getPrivateChatMessages(for peerID: PeerID) -> [BitchatMessage] {
|
||||
lifecycleCoordinator.getPrivateChatMessages(for: peerID)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func getPeerIDForNickname(_ nickname: String) -> PeerID? {
|
||||
peerIdentityCoordinator.getPeerIDForNickname(nickname)
|
||||
|
||||
@@ -35,11 +35,6 @@ extension ChatViewModel {
|
||||
nostrCoordinator.inbound.handleNostrEvent(event)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func subscribeToGeoChat(_ ch: GeohashChannel) {
|
||||
nostrCoordinator.subscriptions.subscribeToGeoChat(ch)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleGiftWrap(_ giftWrap: NostrEvent, id: NostrIdentity) {
|
||||
nostrCoordinator.inbound.handleGiftWrap(giftWrap, id: id)
|
||||
@@ -55,21 +50,11 @@ extension ChatViewModel {
|
||||
nostrCoordinator.subscriptions.beginGeohashSampling(for: geohashes)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func subscribe(_ gh: String) {
|
||||
nostrCoordinator.subscriptions.subscribe(gh)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func subscribeNostrEvent(_ event: NostrEvent, gh: String) {
|
||||
nostrCoordinator.presence.subscribeNostrEvent(event, gh: gh)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func cooldownPerGeohash(_ gh: String, content: String, event: NostrEvent) {
|
||||
nostrCoordinator.presence.cooldownPerGeohash(gh, content: content, event: event)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func endGeohashSampling() {
|
||||
nostrCoordinator.subscriptions.endGeohashSampling()
|
||||
@@ -80,35 +65,11 @@ extension ChatViewModel {
|
||||
nostrCoordinator.subscriptions.setupNostrMessageHandling()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleNostrMessage(_ giftWrap: NostrEvent) {
|
||||
nostrCoordinator.inbound.handleNostrMessage(giftWrap)
|
||||
}
|
||||
|
||||
func processNostrMessage(_ giftWrap: NostrEvent) async {
|
||||
await nostrCoordinator.inbound.processNostrMessage(giftWrap)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func findNoiseKey(for nostrPubkey: String) -> Data? {
|
||||
nostrCoordinator.inbound.findNoiseKey(for: nostrPubkey)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func sendDeliveryAckViaNostrEmbedded(
|
||||
_ message: BitchatMessage,
|
||||
wasReadBefore: Bool,
|
||||
senderPubkey: String,
|
||||
key: Data?
|
||||
) {
|
||||
nostrCoordinator.sendDeliveryAckViaNostrEmbedded(
|
||||
message,
|
||||
wasReadBefore: wasReadBefore,
|
||||
senderPubkey: senderPubkey,
|
||||
key: key
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func sendFavoriteNotificationViaNostr(noisePublicKey: Data, isFavorite: Bool) {
|
||||
nostrCoordinator.sendFavoriteNotificationViaNostr(noisePublicKey: noisePublicKey, isFavorite: isFavorite)
|
||||
|
||||
@@ -55,16 +55,6 @@ extension ChatViewModel {
|
||||
privateConversationCoordinator.handleReadReceipt(payload, senderPubkey: senderPubkey, convKey: convKey)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func sendDeliveryAckIfNeeded(to messageId: String, senderPubKey: String, from id: NostrIdentity) {
|
||||
privateConversationCoordinator.sendDeliveryAckIfNeeded(to: messageId, senderPubKey: senderPubKey, from: id)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func sendReadReceiptIfNeeded(to messageId: String, senderPubKey: String, from id: NostrIdentity) {
|
||||
privateConversationCoordinator.sendReadReceiptIfNeeded(to: messageId, senderPubKey: senderPubKey, from: id)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func sendVoiceNote(at url: URL) {
|
||||
mediaTransferCoordinator.sendVoiceNote(at: url)
|
||||
@@ -162,11 +152,6 @@ extension ChatViewModel {
|
||||
mediaTransferCoordinator.clearTransferMapping(for: messageID)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleMediaSendFailure(messageID: String, reason: String) {
|
||||
mediaTransferCoordinator.handleMediaSendFailure(messageID: messageID, reason: reason)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleTransferEvent(_ event: TransferProgressManager.Event) {
|
||||
mediaTransferCoordinator.handleTransferEvent(event)
|
||||
@@ -194,59 +179,6 @@ extension ChatViewModel {
|
||||
privateConversationCoordinator.handlePrivateMessage(message)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func isDuplicateMessage(_ messageId: String, targetPeerID: PeerID) -> Bool {
|
||||
privateConversationCoordinator.isDuplicateMessage(messageId, targetPeerID: targetPeerID)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func addMessageToPrivateChatsIfNeeded(_ message: BitchatMessage, targetPeerID: PeerID) {
|
||||
privateConversationCoordinator.addMessageToPrivateChatsIfNeeded(message, targetPeerID: targetPeerID)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func mirrorToEphemeralIfNeeded(_ message: BitchatMessage, targetPeerID: PeerID, key: Data?) {
|
||||
privateConversationCoordinator.mirrorToEphemeralIfNeeded(message, targetPeerID: targetPeerID, key: key)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleViewingThisChat(_ message: BitchatMessage, targetPeerID: PeerID, key: Data?, senderPubkey: String) {
|
||||
privateConversationCoordinator.handleViewingThisChat(
|
||||
message,
|
||||
targetPeerID: targetPeerID,
|
||||
key: key,
|
||||
senderPubkey: senderPubkey
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func markAsUnreadIfNeeded(
|
||||
shouldMarkAsUnread: Bool,
|
||||
targetPeerID: PeerID,
|
||||
key: Data?,
|
||||
isRecentMessage: Bool,
|
||||
senderNickname: String,
|
||||
messageContent: String
|
||||
) {
|
||||
privateConversationCoordinator.markAsUnreadIfNeeded(
|
||||
shouldMarkAsUnread: shouldMarkAsUnread,
|
||||
targetPeerID: targetPeerID,
|
||||
key: key,
|
||||
isRecentMessage: isRecentMessage,
|
||||
senderNickname: senderNickname,
|
||||
messageContent: messageContent
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func handleFavoriteNotification(_ content: String, from peerID: PeerID, senderNickname: String) {
|
||||
privateConversationCoordinator.handleFavoriteNotification(
|
||||
content,
|
||||
from: peerID,
|
||||
senderNickname: senderNickname
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func processActionMessage(_ message: BitchatMessage) -> BitchatMessage {
|
||||
privateConversationCoordinator.processActionMessage(message)
|
||||
@@ -257,11 +189,6 @@ extension ChatViewModel {
|
||||
privateConversationCoordinator.migratePrivateChatsIfNeeded(for: peerID, senderNickname: senderNickname)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func sendFavoriteNotification(to peerID: PeerID, isFavorite: Bool) {
|
||||
privateConversationCoordinator.sendFavoriteNotification(to: peerID, isFavorite: isFavorite)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func isMessageBlocked(_ message: BitchatMessage) -> Bool {
|
||||
privateConversationCoordinator.isMessageBlocked(message)
|
||||
|
||||
@@ -54,7 +54,7 @@ extension ChatViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleTorPreferenceChanged(_ notification: Notification) {
|
||||
@objc func handleTorPreferenceChanged(_: Notification) {
|
||||
Task { @MainActor in
|
||||
self.torStatusAnnounced = false
|
||||
self.torInitialReadyAnnounced = false
|
||||
|
||||
@@ -79,9 +79,4 @@ struct MessageRateLimiter {
|
||||
|
||||
return senderAllowed && contentAllowed
|
||||
}
|
||||
|
||||
mutating func reset() {
|
||||
senderBuckets.removeAll()
|
||||
contentBuckets.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,13 +82,6 @@ final class MinimalDistancePalette {
|
||||
return Color(hue: entry.hue, saturation: saturation, brightness: brightness)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func reset() {
|
||||
currentSeeds.removeAll()
|
||||
entries.removeAll()
|
||||
previousEntries.removeAll()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func rebuildEntries() {
|
||||
guard !currentSeeds.isEmpty else {
|
||||
|
||||
@@ -14,15 +14,15 @@ import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol PublicMessagePipelineDelegate: AnyObject {
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, normalizeContent content: String) -> String
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, contentTimestampForKey key: String) -> Date?
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, recordContentKey key: String, timestamp: Date)
|
||||
func pipeline(_: PublicMessagePipeline, normalizeContent content: String) -> String
|
||||
func pipeline(_: PublicMessagePipeline, contentTimestampForKey key: String) -> Date?
|
||||
func pipeline(_: PublicMessagePipeline, recordContentKey key: String, timestamp: Date)
|
||||
/// Commits a batched message to its conversation in the store.
|
||||
/// Returns `false` when the message was already present (ID dedup).
|
||||
@discardableResult
|
||||
func pipeline(_ pipeline: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool
|
||||
func pipelinePrewarmMessage(_ pipeline: PublicMessagePipeline, message: BitchatMessage)
|
||||
func pipelineSetBatchingState(_ pipeline: PublicMessagePipeline, isBatching: Bool)
|
||||
func pipeline(_: PublicMessagePipeline, commit message: BitchatMessage, to conversationID: ConversationID) -> Bool
|
||||
func pipelinePrewarmMessage(_: PublicMessagePipeline, message: BitchatMessage)
|
||||
func pipelineSetBatchingState(_: PublicMessagePipeline, isBatching: Bool)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -15,8 +15,6 @@ struct AppInfoView: View {
|
||||
AppTheme(rawValue: appThemeRawValue) ?? .matrix
|
||||
}
|
||||
|
||||
private var backgroundColor: Color { palette.background }
|
||||
|
||||
private var textColor: Color { palette.primary }
|
||||
|
||||
private var secondaryTextColor: Color { palette.secondary }
|
||||
|
||||
@@ -11,8 +11,6 @@ struct ContentPeopleSheetView: View {
|
||||
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
|
||||
@EnvironmentObject private var verificationModel: VerificationModel
|
||||
@EnvironmentObject private var conversationUIModel: ConversationUIModel
|
||||
@EnvironmentObject private var locationChannelsModel: LocationChannelsModel
|
||||
@EnvironmentObject private var peerListModel: PeerListModel
|
||||
|
||||
@Binding var showSidebar: Bool
|
||||
@Binding var messageText: String
|
||||
@@ -79,8 +77,7 @@ struct ContentPeopleSheetView: View {
|
||||
#endif
|
||||
} else {
|
||||
ContentPeopleListView(
|
||||
showSidebar: $showSidebar,
|
||||
headerHeight: headerHeight
|
||||
showSidebar: $showSidebar
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -142,8 +139,6 @@ private struct ContentPeopleListView: View {
|
||||
|
||||
@Binding var showSidebar: Bool
|
||||
|
||||
let headerHeight: CGFloat
|
||||
|
||||
@State private var showVerifySheet = false
|
||||
|
||||
var body: some View {
|
||||
@@ -285,7 +280,6 @@ private extension ContentPeopleListView {
|
||||
}
|
||||
|
||||
private struct ContentPrivateChatSheetView: View {
|
||||
@EnvironmentObject private var appChromeModel: AppChromeModel
|
||||
@EnvironmentObject private var privateConversationModel: PrivateConversationModel
|
||||
|
||||
@Binding var showSidebar: Bool
|
||||
|
||||
@@ -17,8 +17,6 @@ struct FingerprintView: View {
|
||||
|
||||
private var textColor: Color { palette.primary }
|
||||
|
||||
private var backgroundColor: Color { palette.background }
|
||||
|
||||
private enum Strings {
|
||||
static let title: LocalizedStringKey = "fingerprint.title"
|
||||
static let theirFingerprint: LocalizedStringKey = "fingerprint.their_label"
|
||||
@@ -45,9 +43,6 @@ struct FingerprintView: View {
|
||||
count
|
||||
)
|
||||
}
|
||||
static func unknownPeer() -> String {
|
||||
String(localized: "common.unknown", comment: "Label for an unknown peer")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -13,8 +13,6 @@ struct LocationChannelsSheet: View {
|
||||
@State private var customGeohash: String = ""
|
||||
@State private var customError: String? = nil
|
||||
|
||||
private var backgroundColor: Color { palette.background }
|
||||
|
||||
private enum Strings {
|
||||
static let title: LocalizedStringKey = "location_channels.title"
|
||||
static let description: LocalizedStringKey = "location_channels.description"
|
||||
@@ -401,7 +399,7 @@ struct LocationChannelsSheet: View {
|
||||
title: String,
|
||||
subtitlePrefix: String,
|
||||
subtitleName: String? = nil,
|
||||
subtitleNameBold: Bool = false,
|
||||
subtitleNameBold _: Bool = false,
|
||||
isSelected: Bool,
|
||||
titleColor: Color? = nil,
|
||||
titleBold: Bool = false,
|
||||
|
||||
@@ -288,7 +288,6 @@ struct VerificationSheetView: View {
|
||||
@State private var showingScanner = false
|
||||
@ThemedPalette private var palette
|
||||
|
||||
private var backgroundColor: Color { palette.background }
|
||||
private var accentColor: Color { palette.accent }
|
||||
private var boxColor: Color { palette.secondary.opacity(0.1) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user