mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 07:05:19 +00:00
Gate Tor/Nostr start behind permissions or mutual favorites; add clean shutdown + robust gating (#619)
- Add NetworkActivationService to permit Tor/Nostr only when location is authorized OR at least one mutual favorite exists. - Gate TorManager.startIfNeeded/ensureRunningOnForeground behind a global allowAutoStart flag. - Always stop Tor on background for deterministic restarts; rebuild sessions on foreground when allowed. - NostrRelayManager respects the gate in connect/ensureConnections/subscribe/send/connectToRelay and skips reconnection when disallowed. - Symmetric shutdown when conditions become disallowed: disconnect relays and stop Tor. - Fix double-start by avoiding restart if Tor is already ready; prevent background thrash. - Improve UX: post "starting tor…" via TorWillStart, and "tor started…" on initial ready; keep existing restart messages. Rationale: Avoid starting Tor/relays when the user has no location permission and no mutual favorites, and ensure a clean, predictable lifecycle (no stale sockets, no double starts). Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -57,8 +57,8 @@ struct BitchatApp: App {
|
|||||||
#elseif os(macOS)
|
#elseif os(macOS)
|
||||||
appDelegate.chatViewModel = chatViewModel
|
appDelegate.chatViewModel = chatViewModel
|
||||||
#endif
|
#endif
|
||||||
// Spin up Tor early; all internet will gate on Tor 100%
|
// Initialize network activation policy; will start Tor/Nostr only when allowed
|
||||||
TorManager.shared.startIfNeeded()
|
NetworkActivationService.shared.start()
|
||||||
// Check for shared content
|
// Check for shared content
|
||||||
checkForSharedContent()
|
checkForSharedContent()
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ struct BitchatApp: App {
|
|||||||
switch newPhase {
|
switch newPhase {
|
||||||
case .background:
|
case .background:
|
||||||
// Keep BLE mesh running in background; BLEService adapts scanning automatically
|
// Keep BLE mesh running in background; BLEService adapts scanning automatically
|
||||||
// Optionally nudge Tor to dormant to save power
|
// Always send Tor to dormant on background for a clean restart later.
|
||||||
TorManager.shared.setAppForeground(false)
|
TorManager.shared.setAppForeground(false)
|
||||||
TorManager.shared.goDormantOnBackground()
|
TorManager.shared.goDormantOnBackground()
|
||||||
// Stop geohash sampling while backgrounded
|
// Stop geohash sampling while backgrounded
|
||||||
@@ -87,11 +87,14 @@ struct BitchatApp: App {
|
|||||||
// On initial cold launch, Tor was just started in onAppear.
|
// On initial cold launch, Tor was just started in onAppear.
|
||||||
// Skip the deterministic restart the first time we become active.
|
// Skip the deterministic restart the first time we become active.
|
||||||
if didHandleInitialActive && didEnterBackground {
|
if didHandleInitialActive && didEnterBackground {
|
||||||
|
if TorManager.shared.isAutoStartAllowed() && !TorManager.shared.isReady {
|
||||||
TorManager.shared.ensureRunningOnForeground()
|
TorManager.shared.ensureRunningOnForeground()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
didHandleInitialActive = true
|
didHandleInitialActive = true
|
||||||
}
|
}
|
||||||
didEnterBackground = false
|
didEnterBackground = false
|
||||||
|
if TorManager.shared.isAutoStartAllowed() {
|
||||||
Task.detached {
|
Task.detached {
|
||||||
let _ = await TorManager.shared.awaitReady(timeout: 60)
|
let _ = await TorManager.shared.awaitReady(timeout: 60)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
@@ -101,6 +104,7 @@ struct BitchatApp: App {
|
|||||||
NostrRelayManager.shared.resetAllConnections()
|
NostrRelayManager.shared.resetAllConnections()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
checkForSharedContent()
|
checkForSharedContent()
|
||||||
case .inactive:
|
case .inactive:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
|
|
||||||
/// Connect to all configured relays
|
/// Connect to all configured relays
|
||||||
func connect() {
|
func connect() {
|
||||||
|
// Global network policy gate
|
||||||
|
if !TorManager.shared.isAutoStartAllowed() { return }
|
||||||
// Ensure Tor is started early and wait for readiness off-main; then hop back to connect.
|
// Ensure Tor is started early and wait for readiness off-main; then hop back to connect.
|
||||||
Task.detached {
|
Task.detached {
|
||||||
let ready = await TorManager.shared.awaitReady()
|
let ready = await TorManager.shared.awaitReady()
|
||||||
@@ -117,6 +119,8 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
|
|
||||||
/// Ensure connections exist to the given relay URLs (idempotent).
|
/// Ensure connections exist to the given relay URLs (idempotent).
|
||||||
func ensureConnections(to relayUrls: [String]) {
|
func ensureConnections(to relayUrls: [String]) {
|
||||||
|
// Global network policy gate
|
||||||
|
if !TorManager.shared.isAutoStartAllowed() { return }
|
||||||
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
||||||
// Defer until Tor is fully ready; avoid queuing connection attempts early
|
// Defer until Tor is fully ready; avoid queuing connection attempts early
|
||||||
Task.detached { [weak self] in
|
Task.detached { [weak self] in
|
||||||
@@ -139,6 +143,8 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
|
|
||||||
/// Send an event to specified relays (or all if none specified)
|
/// Send an event to specified relays (or all if none specified)
|
||||||
func sendEvent(_ event: NostrEvent, to relayUrls: [String]? = nil) {
|
func sendEvent(_ event: NostrEvent, to relayUrls: [String]? = nil) {
|
||||||
|
// Global network policy gate
|
||||||
|
if !TorManager.shared.isAutoStartAllowed() { return }
|
||||||
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
if TorManager.shared.torEnforced && !TorManager.shared.isReady {
|
||||||
// Defer sends until Tor is ready to avoid premature queueing
|
// Defer sends until Tor is ready to avoid premature queueing
|
||||||
Task.detached { [weak self] in
|
Task.detached { [weak self] in
|
||||||
@@ -213,6 +219,8 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
handler: @escaping (NostrEvent) -> Void,
|
handler: @escaping (NostrEvent) -> Void,
|
||||||
onEOSE: (() -> Void)? = nil
|
onEOSE: (() -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
|
// Global network policy gate
|
||||||
|
if !TorManager.shared.isAutoStartAllowed() { return }
|
||||||
// Coalesce rapid duplicate subscribe requests only if a handler already exists
|
// Coalesce rapid duplicate subscribe requests only if a handler already exists
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if messageHandlers[id] != nil {
|
if messageHandlers[id] != nil {
|
||||||
@@ -317,6 +325,8 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
|
|
||||||
private func connectToRelay(_ urlString: String) {
|
private func connectToRelay(_ urlString: String) {
|
||||||
|
// Global network policy gate
|
||||||
|
if !TorManager.shared.isAutoStartAllowed() { return }
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
SecureLogger.warning("Invalid relay URL: \(urlString)", category: .session)
|
SecureLogger.warning("Invalid relay URL: \(urlString)", category: .session)
|
||||||
return
|
return
|
||||||
@@ -523,6 +533,13 @@ final class NostrRelayManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleDisconnection(relayUrl: String, error: Error) {
|
private func handleDisconnection(relayUrl: String, error: Error) {
|
||||||
|
// If networking is disallowed, do not schedule reconnection
|
||||||
|
if !TorManager.shared.isAutoStartAllowed() {
|
||||||
|
connections.removeValue(forKey: relayUrl)
|
||||||
|
subscriptions.removeValue(forKey: relayUrl)
|
||||||
|
updateRelayStatus(relayUrl, isConnected: false, error: error)
|
||||||
|
return
|
||||||
|
}
|
||||||
connections.removeValue(forKey: relayUrl)
|
connections.removeValue(forKey: relayUrl)
|
||||||
subscriptions.removeValue(forKey: relayUrl)
|
subscriptions.removeValue(forKey: relayUrl)
|
||||||
updateRelayStatus(relayUrl, isConnected: false, error: error)
|
updateRelayStatus(relayUrl, isConnected: false, error: error)
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import Foundation
|
||||||
|
import BitLogger
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Coordinates when the app is allowed to start Tor and connect to Nostr relays.
|
||||||
|
/// Policy: permit start when either location permissions are authorized OR
|
||||||
|
/// there exists at least one mutual favorite. Otherwise, do not start.
|
||||||
|
@MainActor
|
||||||
|
final class NetworkActivationService: ObservableObject {
|
||||||
|
static let shared = NetworkActivationService()
|
||||||
|
|
||||||
|
@Published private(set) var activationAllowed: Bool = false
|
||||||
|
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private var started = false
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
guard !started else { return }
|
||||||
|
started = true
|
||||||
|
|
||||||
|
// Initial compute
|
||||||
|
activationAllowed = Self.computeAllowed()
|
||||||
|
TorManager.shared.setAutoStartAllowed(activationAllowed)
|
||||||
|
|
||||||
|
// React to location permission changes
|
||||||
|
LocationChannelManager.shared.$permissionState
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.reevaluate()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// React to mutual favorites changes
|
||||||
|
FavoritesPersistenceService.shared.$mutualFavorites
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.reevaluate()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reevaluate() {
|
||||||
|
let allowed = Self.computeAllowed()
|
||||||
|
if allowed != activationAllowed {
|
||||||
|
SecureLogger.info("NetworkActivationService: activationAllowed -> \(allowed)", category: .session)
|
||||||
|
activationAllowed = allowed
|
||||||
|
TorManager.shared.setAutoStartAllowed(allowed)
|
||||||
|
if allowed {
|
||||||
|
// Kick Tor + relays if we're now permitted
|
||||||
|
TorManager.shared.startIfNeeded()
|
||||||
|
// If app is in foreground, begin relay connections
|
||||||
|
if TorManager.shared.isForeground() {
|
||||||
|
NostrRelayManager.shared.connect()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Transitioned to disallowed: disconnect relays and shut down Tor
|
||||||
|
NostrRelayManager.shared.disconnect()
|
||||||
|
TorManager.shared.goDormantOnBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func computeAllowed() -> Bool {
|
||||||
|
let permOK = LocationChannelManager.shared.permissionState == .authorized
|
||||||
|
let hasMutual = !FavoritesPersistenceService.shared.mutualFavorites.isEmpty
|
||||||
|
return permOK || hasMutual
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,18 +69,24 @@ final class TorManager: ObservableObject {
|
|||||||
private var pathMonitor: NWPathMonitor?
|
private var pathMonitor: NWPathMonitor?
|
||||||
private var isAppForeground: Bool = true
|
private var isAppForeground: Bool = true
|
||||||
private var lastRestartAt: Date? = nil
|
private var lastRestartAt: Date? = nil
|
||||||
|
// Global policy gate: only allow Tor to start when true
|
||||||
|
private(set) var allowAutoStart: Bool = false
|
||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
// MARK: - Public API
|
// MARK: - Public API
|
||||||
|
|
||||||
func startIfNeeded() {
|
func startIfNeeded() {
|
||||||
|
// Respect global start policy
|
||||||
|
guard allowAutoStart else { return }
|
||||||
// Do not start in background; caller should wait for foreground
|
// Do not start in background; caller should wait for foreground
|
||||||
guard isAppForeground else { return }
|
guard isAppForeground else { return }
|
||||||
guard !didStart else { return }
|
guard !didStart else { return }
|
||||||
didStart = true
|
didStart = true
|
||||||
isStarting = true
|
isStarting = true
|
||||||
lastError = nil
|
lastError = nil
|
||||||
|
// Announce initial start so UI can show a status message
|
||||||
|
NotificationCenter.default.post(name: .TorWillStart, object: nil)
|
||||||
ensureFilesystemLayout()
|
ensureFilesystemLayout()
|
||||||
startTor()
|
startTor()
|
||||||
startPathMonitorIfNeeded()
|
startPathMonitorIfNeeded()
|
||||||
@@ -473,6 +479,8 @@ final class TorManager: ObservableObject {
|
|||||||
// MARK: - Foreground recovery and control helpers
|
// MARK: - Foreground recovery and control helpers
|
||||||
|
|
||||||
func ensureRunningOnForeground() {
|
func ensureRunningOnForeground() {
|
||||||
|
// Respect global start policy
|
||||||
|
if !allowAutoStart { return }
|
||||||
// iOS can suspend Tor harshly; the most reliable approach for
|
// iOS can suspend Tor harshly; the most reliable approach for
|
||||||
// embedding is to restart Tor every time we become active.
|
// embedding is to restart Tor every time we become active.
|
||||||
Task.detached(priority: .userInitiated) { [weak self] in
|
Task.detached(priority: .userInitiated) { [weak self] in
|
||||||
@@ -636,3 +644,14 @@ final class TorManager: ObservableObject {
|
|||||||
return resultText
|
return resultText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Start policy configuration
|
||||||
|
extension TorManager {
|
||||||
|
@MainActor
|
||||||
|
func setAutoStartAllowed(_ allow: Bool) {
|
||||||
|
allowAutoStart = allow
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func isAutoStartAllowed() -> Bool { allowAutoStart }
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ import Foundation
|
|||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let TorDidBecomeReady = Notification.Name("TorDidBecomeReady")
|
static let TorDidBecomeReady = Notification.Name("TorDidBecomeReady")
|
||||||
static let TorWillRestart = Notification.Name("TorWillRestart")
|
static let TorWillRestart = Notification.Name("TorWillRestart")
|
||||||
|
static let TorWillStart = Notification.Name("TorWillStart")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,6 +482,8 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
|
|
||||||
// Track app startup phase to prevent marking old messages as unread
|
// Track app startup phase to prevent marking old messages as unread
|
||||||
private var isStartupPhase = true
|
private var isStartupPhase = true
|
||||||
|
// Announce Tor initial readiness once per launch to avoid duplicates
|
||||||
|
private var torInitialReadyAnnounced: Bool = false
|
||||||
|
|
||||||
// Track Nostr pubkey mappings for unknown senders
|
// Track Nostr pubkey mappings for unknown senders
|
||||||
private var nostrKeyMapping: [String: String] = [:] // senderPeerID -> nostrPubkey
|
private var nostrKeyMapping: [String: String] = [:] // senderPeerID -> nostrPubkey
|
||||||
@@ -548,52 +550,28 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Start mesh service immediately
|
// Start mesh service immediately
|
||||||
meshService.startServices()
|
meshService.startServices()
|
||||||
|
|
||||||
// Announce Tor status (geohash-only; do not show in mesh chat)
|
// Announce Tor status (geohash-only; do not show in mesh chat). Only when auto-start is allowed.
|
||||||
if TorManager.shared.torEnforced && !torStatusAnnounced {
|
if TorManager.shared.torEnforced && !torStatusAnnounced && TorManager.shared.isAutoStartAllowed() {
|
||||||
torStatusAnnounced = true
|
torStatusAnnounced = true
|
||||||
addGeohashOnlySystemMessage("starting tor...")
|
addGeohashOnlySystemMessage("starting tor...")
|
||||||
// Suppress incremental Tor progress messages; only show initial start and readiness.
|
// Suppress incremental Tor progress messages
|
||||||
torProgressCancellable = nil
|
torProgressCancellable = nil
|
||||||
Task.detached {
|
|
||||||
let ready = await TorManager.shared.awaitReady()
|
|
||||||
Task { @MainActor [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
if ready { self.addGeohashOnlySystemMessage("tor started. routing all chats via tor for privacy.") }
|
|
||||||
else { self.addGeohashOnlySystemMessage("still waiting for tor to start...") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !TorManager.shared.torEnforced && !torStatusAnnounced {
|
} else if !TorManager.shared.torEnforced && !torStatusAnnounced {
|
||||||
torStatusAnnounced = true
|
torStatusAnnounced = true
|
||||||
addGeohashOnlySystemMessage("development build: tor bypass enabled.")
|
addGeohashOnlySystemMessage("development build: tor bypass enabled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Nostr services only after Tor is fully ready, to avoid
|
// Initialize Nostr relay manager regardless of Tor readiness; connection is controlled elsewhere
|
||||||
// interleaving setup logs during Tor startup.
|
|
||||||
Task { @MainActor in
|
|
||||||
// Wait for Tor readiness before Nostr init
|
|
||||||
let ready = await TorManager.shared.awaitReady(timeout: 60)
|
|
||||||
guard ready else {
|
|
||||||
SecureLogger.error("Nostr init skipped: Tor not ready", category: .session)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nostrRelayManager = NostrRelayManager.shared
|
nostrRelayManager = NostrRelayManager.shared
|
||||||
SecureLogger.debug("Initializing Nostr relay connections", category: .session)
|
// Attempt to flush any queued outbox (mesh/Nostr routing will gate appropriately)
|
||||||
// Connect is managed centrally on scene activation; avoid duplicate connects here
|
|
||||||
|
|
||||||
// Attempt to flush any queued outbox after Nostr comes online
|
|
||||||
messageRouter.flushAllOutbox()
|
messageRouter.flushAllOutbox()
|
||||||
|
// End startup phase after a short delay
|
||||||
// End startup phase after 2 seconds
|
|
||||||
// During startup phase, we:
|
|
||||||
// 1. Skip cleanup of read receipts
|
|
||||||
// 2. Only block OLD messages from being marked as unread
|
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
try? await Task.sleep(nanoseconds: UInt64(TransportConfig.uiStartupPhaseDurationSeconds * 1_000_000_000)) // 2 seconds
|
try? await Task.sleep(nanoseconds: UInt64(TransportConfig.uiStartupPhaseDurationSeconds * 1_000_000_000))
|
||||||
self.isStartupPhase = false
|
self.isStartupPhase = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind unified peer service's peer list to our published property
|
// Bind unified peer service's peer list to our published property
|
||||||
let cancellable = unifiedPeerService.$peers
|
let peersCancellable = unifiedPeerService.$peers
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] peers in
|
.sink { [weak self] peers in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -611,18 +589,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.peerIndex = uniquePeers
|
self.peerIndex = uniquePeers
|
||||||
// Schedule UI update if peers changed
|
|
||||||
if peers.count > 0 || self.allPeers.count > 0 {
|
|
||||||
// UI will update automatically
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update private chat peer ID if needed when peers change
|
// Update private chat peer ID if needed when peers change
|
||||||
if self.selectedPrivateChatFingerprint != nil {
|
if self.selectedPrivateChatFingerprint != nil {
|
||||||
self.updatePrivateChatPeerIfNeeded()
|
self.updatePrivateChatPeerIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.cancellables.insert(peersCancellable)
|
||||||
self.cancellables.insert(cancellable)
|
|
||||||
|
|
||||||
// Resubscribe geohash on relay reconnect
|
// Resubscribe geohash on relay reconnect
|
||||||
if let relayMgr = self.nostrRelayManager {
|
if let relayMgr = self.nostrRelayManager {
|
||||||
@@ -652,7 +624,6 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Set up Noise encryption callbacks
|
// Set up Noise encryption callbacks
|
||||||
setupNoiseCallbacks()
|
setupNoiseCallbacks()
|
||||||
@@ -801,6 +772,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
name: .TorDidBecomeReady,
|
name: .TorDidBecomeReady,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleTorWillStart),
|
||||||
|
name: .TorWillStart,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
#else
|
#else
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
@@ -845,6 +822,12 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
name: .TorDidBecomeReady,
|
name: .TorDidBecomeReady,
|
||||||
object: nil
|
object: nil
|
||||||
)
|
)
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleTorWillStart),
|
||||||
|
name: .TorWillStart,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,6 +838,15 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Tor notifications
|
// MARK: - Tor notifications
|
||||||
|
@objc private func handleTorWillStart() {
|
||||||
|
Task { @MainActor in
|
||||||
|
if !self.torStatusAnnounced && TorManager.shared.torEnforced {
|
||||||
|
self.torStatusAnnounced = true
|
||||||
|
// Post only in geohash channels (queue if not active)
|
||||||
|
self.addGeohashOnlySystemMessage("starting tor...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@objc private func handleTorWillRestart() {
|
@objc private func handleTorWillRestart() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self.torRestartPending = true
|
self.torRestartPending = true
|
||||||
@@ -870,6 +862,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Post only in geohash channels (queue if not active)
|
// Post only in geohash channels (queue if not active)
|
||||||
self.addGeohashOnlySystemMessage("tor restarted. network routing restored.")
|
self.addGeohashOnlySystemMessage("tor restarted. network routing restored.")
|
||||||
self.torRestartPending = false
|
self.torRestartPending = false
|
||||||
|
} else if TorManager.shared.torEnforced && !self.torInitialReadyAnnounced {
|
||||||
|
// Initial start completed
|
||||||
|
self.addGeohashOnlySystemMessage("tor started. routing all chats via tor for privacy.")
|
||||||
|
self.torInitialReadyAnnounced = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user