mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 21:05:20 +00:00
Compare commits
4
Commits
file-transfer
...
v1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56e7324069 | ||
|
|
1733dda6cd | ||
|
|
00ff5fd31c | ||
|
|
f684c452b2 |
@@ -1,4 +1,4 @@
|
||||
MARKETING_VERSION = 1.4.0
|
||||
MARKETING_VERSION = 1.4.1
|
||||
CURRENT_PROJECT_VERSION = 1
|
||||
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0
|
||||
|
||||
@@ -14,7 +14,6 @@ default:
|
||||
# Check prerequisites
|
||||
check:
|
||||
@echo "Checking prerequisites..."
|
||||
@command -v xcodegen >/dev/null 2>&1 || (echo "❌ XcodeGen not found. Install with: brew install xcodegen" && exit 1)
|
||||
@command -v xcodebuild >/dev/null 2>&1 || (echo "❌ Xcode not found. Install Xcode from App Store" && exit 1)
|
||||
@security find-identity -v -p codesigning | grep -q "Developer ID" || (echo "⚠️ No Developer ID found - code signing may fail" && exit 0)
|
||||
@echo "✅ All prerequisites met"
|
||||
@@ -22,8 +21,6 @@ check:
|
||||
# Backup original files
|
||||
backup:
|
||||
@echo "Backing up original project configuration..."
|
||||
@cp project.yml project.yml.backup 2>/dev/null || true
|
||||
@# Backup other files that get modified by xcodegen
|
||||
@if [ -f bitchat.xcodeproj/project.pbxproj ]; then cp bitchat.xcodeproj/project.pbxproj bitchat.xcodeproj/project.pbxproj.backup; fi
|
||||
@if [ -f bitchat/Info.plist ]; then cp bitchat/Info.plist bitchat/Info.plist.backup; fi
|
||||
|
||||
@@ -44,15 +41,10 @@ patch-for-macos: backup
|
||||
@# Move iOS-specific files out of the way temporarily
|
||||
@if [ -f bitchat/LaunchScreen.storyboard ]; then mv bitchat/LaunchScreen.storyboard bitchat/LaunchScreen.storyboard.ios; fi
|
||||
|
||||
# Generate Xcode project with patches
|
||||
generate: patch-for-macos
|
||||
@echo "Generating Xcode project..."
|
||||
@xcodegen generate
|
||||
|
||||
# Build the macOS app
|
||||
build: check generate
|
||||
build: #check generate
|
||||
@echo "Building BitChat for macOS..."
|
||||
@xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build
|
||||
@xcodebuild -project bitchat.xcodeproj -scheme "bitchat_macOS" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build
|
||||
|
||||
# Run the macOS app
|
||||
run: build
|
||||
@@ -75,9 +67,7 @@ clean: restore
|
||||
# Quick run without cleaning (for development)
|
||||
dev-run: check
|
||||
@echo "Quick development build..."
|
||||
@if [ ! -f project.yml.backup ]; then just patch-for-macos; fi
|
||||
@xcodegen generate
|
||||
@xcodebuild -project bitchat.xcodeproj -scheme "bitchat (macOS)" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build
|
||||
@xcodebuild -project bitchat.xcodeproj -scheme "bitchat_macOS" -configuration Debug CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" build
|
||||
@find ~/Library/Developer/Xcode/DerivedData -name "bitchat.app" -path "*/Debug/*" -not -path "*/Index.noindex/*" | head -1 | xargs -I {} open "{}"
|
||||
|
||||
# Show app info
|
||||
@@ -106,11 +96,9 @@ nuke:
|
||||
@echo "🧨 Nuclear clean - removing all build artifacts and backups..."
|
||||
@rm -rf ~/Library/Developer/Xcode/DerivedData/bitchat-* 2>/dev/null || true
|
||||
@rm -rf bitchat.xcodeproj 2>/dev/null || true
|
||||
@rm -f project.yml.backup 2>/dev/null || true
|
||||
@rm -f project-macos.yml 2>/dev/null || true
|
||||
@rm -f bitchat.xcodeproj/project.pbxproj.backup 2>/dev/null || true
|
||||
@rm -f bitchat/Info.plist.backup 2>/dev/null || true
|
||||
@# Restore iOS-specific files if they were moved
|
||||
@if [ -f bitchat/LaunchScreen.storyboard.ios ]; then mv bitchat/LaunchScreen.storyboard.ios bitchat/LaunchScreen.storyboard; fi
|
||||
@git checkout -- project.yml bitchat.xcodeproj/project.pbxproj bitchat/Info.plist 2>/dev/null || echo "⚠️ Not a git repo or no changes to restore"
|
||||
@git checkout bitchat.xcodeproj/project.pbxproj bitchat/Info.plist 2>/dev/null || echo "⚠️ Not a git repo or no changes to restore"
|
||||
@echo "✅ Nuclear clean complete"
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -514,7 +514,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = bitchat;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -574,7 +574,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = bitchat;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -609,7 +609,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
|
||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = bitchat;
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -700,7 +700,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
|
||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = bitchat;
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -795,7 +795,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||
MARKETING_VERSION = 1.4.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ShareExtension";
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
|
||||
@@ -210,6 +210,15 @@ final class NoiseRateLimiter {
|
||||
self.messageTimestamps.removeValue(forKey: peerID)
|
||||
}
|
||||
}
|
||||
|
||||
func resetAll() {
|
||||
queue.async(flags: .barrier) {
|
||||
self.handshakeTimestamps.removeAll()
|
||||
self.messageTimestamps.removeAll()
|
||||
self.globalHandshakeTimestamps.removeAll()
|
||||
self.globalMessageTimestamps.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Security Errors
|
||||
|
||||
@@ -308,6 +308,15 @@ final class NoiseSessionManager {
|
||||
_ = sessions.removeValue(forKey: peerID)
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllSessions() {
|
||||
managerQueue.sync(flags: .barrier) {
|
||||
for (_, session) in sessions {
|
||||
session.reset()
|
||||
}
|
||||
sessions.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
func getEstablishedSessions() -> [String: NoiseSession] {
|
||||
return managerQueue.sync {
|
||||
|
||||
@@ -150,13 +150,31 @@ struct NostrIdentityBridge {
|
||||
|
||||
/// Clear all Nostr identity associations and current identity
|
||||
static func clearAllAssociations() {
|
||||
// Delete current Nostr identity
|
||||
KeychainHelper.delete(key: currentIdentityKey, service: keychainService)
|
||||
KeychainHelper.delete(key: deviceSeedKey, service: keychainService)
|
||||
|
||||
// Note: We can't efficiently delete all noise-nostr associations
|
||||
// without tracking them, but they'll be orphaned and eventually cleaned up
|
||||
// The important part is deleting the current identity so a new one is generated
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: keychainService,
|
||||
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||
kSecReturnAttributes as String: true
|
||||
]
|
||||
|
||||
var result: AnyObject?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
if status == errSecSuccess, let items = result as? [[String: Any]] {
|
||||
for item in items {
|
||||
var deleteQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: keychainService
|
||||
]
|
||||
if let account = item[kSecAttrAccount as String] as? String {
|
||||
deleteQuery[kSecAttrAccount as String] = account
|
||||
}
|
||||
SecItemDelete(deleteQuery as CFDictionary)
|
||||
}
|
||||
} else if status == errSecItemNotFound {
|
||||
// nothing persisted; no action needed
|
||||
}
|
||||
|
||||
deviceSeedCache = nil
|
||||
}
|
||||
|
||||
// MARK: - Per-Geohash Identities (Location Channels)
|
||||
|
||||
@@ -89,8 +89,9 @@ final class BLEService: NSObject {
|
||||
|
||||
var myPeerID: String = ""
|
||||
var myNickname: String = "anon"
|
||||
private let noiseService: NoiseEncryptionService
|
||||
private var noiseService: NoiseEncryptionService
|
||||
private let identityManager: SecureIdentityStateManagerProtocol
|
||||
private let keychain: KeychainManagerProtocol
|
||||
private var myPeerIDData: Data = Data()
|
||||
|
||||
// MARK: - Advertising Privacy
|
||||
@@ -330,33 +331,44 @@ final class BLEService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
init(keychain: KeychainManagerProtocol, identityManager: SecureIdentityStateManagerProtocol) {
|
||||
noiseService = NoiseEncryptionService(keychain: keychain)
|
||||
self.identityManager = identityManager
|
||||
super.init()
|
||||
|
||||
// Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars)
|
||||
let fingerprint = noiseService.getIdentityFingerprint() // 64 hex chars
|
||||
self.myPeerID = String(fingerprint.prefix(16))
|
||||
self.myPeerIDData = Data(hexString: myPeerID) ?? Data()
|
||||
|
||||
// Set queue key for identification
|
||||
messageQueue.setSpecific(key: messageQueueKey, value: ())
|
||||
|
||||
// Set up Noise session establishment callback
|
||||
// This ensures we send pending messages only when session is truly established
|
||||
noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in
|
||||
private func configureNoiseServiceCallbacks(for service: NoiseEncryptionService) {
|
||||
service.onPeerAuthenticated = { [weak self] peerID, fingerprint in
|
||||
SecureLogger.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...")
|
||||
// Send any messages that were queued during handshake
|
||||
self?.messageQueue.async { [weak self] in
|
||||
self?.sendPendingMessagesAfterHandshake(for: peerID)
|
||||
self?.sendPendingNoisePayloadsAfterHandshake(for: peerID)
|
||||
}
|
||||
// Proactive presence nudge: announce immediately after handshake
|
||||
self?.messageQueue.async { [weak self] in
|
||||
self?.sendAnnounce(forceSend: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshPeerIdentity() {
|
||||
let fingerprint = noiseService.getIdentityFingerprint()
|
||||
myPeerID = String(fingerprint.prefix(16))
|
||||
myPeerIDData = Data(hexString: myPeerID) ?? Data()
|
||||
}
|
||||
|
||||
private func restartGossipManager() {
|
||||
gossipSyncManager?.stop()
|
||||
let sync = GossipSyncManager(myPeerID: myPeerID)
|
||||
sync.delegate = self
|
||||
sync.start()
|
||||
gossipSyncManager = sync
|
||||
}
|
||||
|
||||
init(keychain: KeychainManagerProtocol, identityManager: SecureIdentityStateManagerProtocol) {
|
||||
self.keychain = keychain
|
||||
noiseService = NoiseEncryptionService(keychain: keychain)
|
||||
self.identityManager = identityManager
|
||||
super.init()
|
||||
|
||||
configureNoiseServiceCallbacks(for: noiseService)
|
||||
refreshPeerIdentity()
|
||||
|
||||
// Set queue key for identification
|
||||
messageQueue.setSpecific(key: messageQueueKey, value: ())
|
||||
|
||||
// Set up application state tracking (iOS only)
|
||||
#if os(iOS)
|
||||
@@ -407,10 +419,7 @@ final class BLEService: NSObject {
|
||||
requestPeerDataPublish()
|
||||
|
||||
// Initialize gossip sync manager
|
||||
let sync = GossipSyncManager(myPeerID: myPeerID)
|
||||
sync.delegate = self
|
||||
sync.start()
|
||||
self.gossipSyncManager = sync
|
||||
restartGossipManager()
|
||||
}
|
||||
|
||||
func setNickname(_ nickname: String) {
|
||||
@@ -738,6 +747,46 @@ final class BLEService: NSObject {
|
||||
subscribedCentrals.removeAll()
|
||||
centralToPeerID.removeAll()
|
||||
}
|
||||
|
||||
func resetIdentityForPanic(currentNickname: String) {
|
||||
messageQueue.sync(flags: .barrier) {
|
||||
pendingMessagesAfterHandshake.removeAll()
|
||||
pendingNoisePayloadsAfterHandshake.removeAll()
|
||||
}
|
||||
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
recentAnnounceBySender.removeAll()
|
||||
recentAnnounceOrder.removeAll()
|
||||
pendingPeripheralWrites.removeAll()
|
||||
pendingNotifications.removeAll()
|
||||
pendingDirectedRelays.removeAll()
|
||||
ingressByMessageID.removeAll()
|
||||
recentPacketTimestamps.removeAll()
|
||||
scheduledRelays.values.forEach { $0.cancel() }
|
||||
scheduledRelays.removeAll()
|
||||
}
|
||||
|
||||
bleQueue.sync {
|
||||
pendingWriteBuffers.removeAll()
|
||||
recentConnectTimeouts.removeAll()
|
||||
}
|
||||
recentDisconnectNotifies.removeAll()
|
||||
|
||||
noiseService.clearEphemeralStateForPanic()
|
||||
noiseService.clearPersistentIdentity()
|
||||
|
||||
let newNoise = NoiseEncryptionService(keychain: keychain)
|
||||
noiseService = newNoise
|
||||
configureNoiseServiceCallbacks(for: newNoise)
|
||||
refreshPeerIdentity()
|
||||
restartGossipManager()
|
||||
|
||||
setNickname(currentNickname)
|
||||
|
||||
messageDeduplicator.reset()
|
||||
requestPeerDataPublish()
|
||||
startServices()
|
||||
}
|
||||
|
||||
func getNoiseService() -> NoiseEncryptionService {
|
||||
return noiseService
|
||||
|
||||
@@ -282,6 +282,7 @@ final class KeychainManager: KeychainManagerProtocol {
|
||||
"com.bitchat.deviceidentity",
|
||||
"com.bitchat.noise.identity",
|
||||
"chat.bitchat.passwords",
|
||||
"chat.bitchat.nostr",
|
||||
"bitchat.keychain",
|
||||
"bitchat",
|
||||
"com.bitchat"
|
||||
|
||||
@@ -526,6 +526,15 @@ final class NoiseEncryptionService {
|
||||
|
||||
SecureLogger.info(.sessionExpired(peerID: peerID))
|
||||
}
|
||||
|
||||
func clearEphemeralStateForPanic() {
|
||||
sessionManager.removeAllSessions()
|
||||
serviceQueue.sync(flags: .barrier) {
|
||||
peerFingerprints.removeAll()
|
||||
fingerprintToPeerID.removeAll()
|
||||
}
|
||||
rateLimiter.resetAll()
|
||||
}
|
||||
|
||||
// MARK: - Private Helpers
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ final class TorManager: ObservableObject {
|
||||
private var controlMonitorStarted = false
|
||||
private var pathMonitor: NWPathMonitor?
|
||||
private var isAppForeground: Bool = true
|
||||
private var isDormant: Bool = false
|
||||
private var lastRestartAt: Date? = nil
|
||||
// Global policy gate: only allow Tor to start when true
|
||||
private(set) var allowAutoStart: Bool = false
|
||||
@@ -83,6 +84,7 @@ final class TorManager: ObservableObject {
|
||||
guard isAppForeground else { return }
|
||||
guard !didStart else { return }
|
||||
didStart = true
|
||||
isDormant = false
|
||||
isStarting = true
|
||||
lastError = nil
|
||||
// Announce initial start so UI can show a status message
|
||||
@@ -492,18 +494,39 @@ final class TorManager: ObservableObject {
|
||||
return true
|
||||
}
|
||||
if !claimed { return }
|
||||
if await self.resumeTorIfPossible() {
|
||||
await MainActor.run {
|
||||
self.restarting = false
|
||||
self.isStarting = false
|
||||
}
|
||||
return
|
||||
}
|
||||
await self.restartTor()
|
||||
await MainActor.run { self.restarting = false }
|
||||
}
|
||||
}
|
||||
|
||||
func goDormantOnBackground() {
|
||||
// Stricter model: fully stop Tor when app backgrounds to save power
|
||||
// and avoid half-suspended states. We'll restart cleanly on .active.
|
||||
// Prefer Tor's DORMANT mode so we can resume on foreground without a full restart.
|
||||
// If the control port is unreachable, fall back to a hard shutdown.
|
||||
Task.detached { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let signaled = await self.controlSendSignal("DORMANT")
|
||||
if signaled {
|
||||
SecureLogger.info("TorManager: signalled DORMANT", category: .session)
|
||||
await MainActor.run {
|
||||
self.isDormant = true
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.isStarting = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
SecureLogger.warning("TorManager: DORMANT signal failed; shutting down", category: .session)
|
||||
_ = tor_host_shutdown()
|
||||
await MainActor.run {
|
||||
self.isDormant = false
|
||||
self.isReady = false
|
||||
self.socksReady = false
|
||||
self.bootstrapProgress = 0
|
||||
@@ -525,14 +548,28 @@ final class TorManager: ObservableObject {
|
||||
self.bootstrapProgress = 0
|
||||
self.bootstrapSummary = ""
|
||||
self.isStarting = true
|
||||
self.isDormant = false
|
||||
self.lastRestartAt = Date()
|
||||
}
|
||||
// Prefer clean shutdown via owning controller FD; join the tor thread
|
||||
_ = tor_host_shutdown()
|
||||
// As a fallback, try control signal if needed (harmless if tor already down)
|
||||
_ = await controlSendSignal("SHUTDOWN")
|
||||
// Allow Tor thread to fully terminate before re-starting.
|
||||
var waited = 0
|
||||
while tor_host_is_running() != 0 && waited < 40 {
|
||||
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
|
||||
waited += 1
|
||||
}
|
||||
if waited >= 40 {
|
||||
SecureLogger.warning("TorManager: tor_host_is_running still true before restart", category: .session)
|
||||
}
|
||||
// Allow control monitor and start logic to reinitialize cleanly
|
||||
await MainActor.run {
|
||||
self.controlMonitorStarted = false
|
||||
self.didStart = false
|
||||
}
|
||||
// Now start fresh
|
||||
await MainActor.run { self.didStart = false }
|
||||
await MainActor.run { self.startIfNeeded() }
|
||||
}
|
||||
|
||||
@@ -598,6 +635,62 @@ final class TorManager: ObservableObject {
|
||||
return (text?.contains("250")) == true
|
||||
}
|
||||
|
||||
private func resumeTorIfPossible() async -> Bool {
|
||||
let wasDormant = await MainActor.run { self.isDormant }
|
||||
let pendingReady = await MainActor.run { self.socksReady && !self.isReady }
|
||||
let needsWake = wasDormant || pendingReady
|
||||
if !needsWake {
|
||||
return false
|
||||
}
|
||||
|
||||
let activated = await controlSendSignal("ACTIVE")
|
||||
let pinged = await controlPingBootstrap(timeout: 3.0)
|
||||
if !activated && !pinged {
|
||||
SecureLogger.warning("TorManager: ACTIVE signal failed", category: .session)
|
||||
return false
|
||||
}
|
||||
|
||||
if let info = await controlGetBootstrapInfo() {
|
||||
await MainActor.run {
|
||||
self.bootstrapProgress = info.progress
|
||||
self.bootstrapSummary = info.summary
|
||||
}
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
self.isDormant = false
|
||||
self.isStarting = true
|
||||
self.socksReady = false
|
||||
}
|
||||
|
||||
let firstReady = await waitForSocksReady(timeout: 12.0)
|
||||
if firstReady {
|
||||
await MainActor.run {
|
||||
self.socksReady = true
|
||||
self.isStarting = false
|
||||
}
|
||||
SecureLogger.info("TorManager: resumed Tor via ACTIVE signal", category: .session)
|
||||
return true
|
||||
}
|
||||
|
||||
if pinged {
|
||||
let secondReady = await waitForSocksReady(timeout: 20.0)
|
||||
await MainActor.run {
|
||||
self.socksReady = secondReady
|
||||
self.isStarting = !secondReady
|
||||
}
|
||||
if secondReady {
|
||||
SecureLogger.info("TorManager: resumed Tor after extended wait", category: .session)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
await MainActor.run { self.isStarting = false }
|
||||
}
|
||||
|
||||
SecureLogger.warning("TorManager: ACTIVE resume failed; will restart", category: .session)
|
||||
return false
|
||||
}
|
||||
|
||||
private func controlExchange(lines: [String], timeout: TimeInterval) async -> String? {
|
||||
guard let cookiePath = dataDirectoryURL()?.appendingPathComponent("control_auth_cookie"),
|
||||
let cookie = try? Data(contentsOf: cookiePath) else { return nil }
|
||||
|
||||
@@ -1467,7 +1467,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
private func sendGeohash(ch: GeohashChannel, content: String) {
|
||||
do {
|
||||
let identity = try NostrIdentityBridge.deriveIdentity(forGeohash: ch.geohash)
|
||||
|
||||
|
||||
let event = try NostrProtocol.createEphemeralGeohashEvent(
|
||||
content: content,
|
||||
geohash: ch.geohash,
|
||||
@@ -1486,9 +1486,11 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
} else {
|
||||
NostrRelayManager.shared.sendEvent(event, to: targetRelays)
|
||||
}
|
||||
|
||||
|
||||
// Track ourselves as active participant
|
||||
recordGeoParticipant(pubkeyHex: identity.publicKeyHex)
|
||||
let shortKey = "nostr:" + identity.publicKeyHex.prefix(TransportConfig.nostrShortKeyDisplayLength)
|
||||
nostrKeyMapping[shortKey] = identity.publicKeyHex
|
||||
SecureLogger.debug("GeoTeleport: sent geo message pub=\(identity.publicKeyHex.prefix(8))… teleported=\(LocationChannelManager.shared.teleported)", category: .session)
|
||||
|
||||
// If we tagged this as teleported, also mark our pubkey in teleportedGeo for UI
|
||||
@@ -1884,6 +1886,33 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
geohashPeople = people
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func isSelfSender(peerID: String?, displayName: String?) -> Bool {
|
||||
guard let peerID else { return false }
|
||||
if peerID == meshService.myPeerID { return true }
|
||||
let lowerPeer = peerID.lowercased()
|
||||
guard lowerPeer.hasPrefix("nostr") else { return false }
|
||||
|
||||
if let mapped = nostrKeyMapping[peerID]?.lowercased(),
|
||||
let gh = currentGeohash,
|
||||
let myIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh) {
|
||||
if mapped == myIdentity.publicKeyHex.lowercased() { return true }
|
||||
}
|
||||
|
||||
if let gh = currentGeohash,
|
||||
let myIdentity = try? NostrIdentityBridge.deriveIdentity(forGeohash: gh) {
|
||||
let myLower = myIdentity.publicKeyHex.lowercased()
|
||||
let shortLen = TransportConfig.nostrShortKeyDisplayLength
|
||||
let shortKey = "nostr:" + myLower.prefix(shortLen)
|
||||
if lowerPeer == shortKey { return true }
|
||||
let suffix = myIdentity.publicKeyHex.suffix(4)
|
||||
let expected = (nickname + "#" + suffix).lowercased()
|
||||
if let display = displayName?.lowercased(), display == expected { return true }
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func startGeoParticipantsTimer() {
|
||||
stopGeoParticipantsTimer()
|
||||
geoParticipantsTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||
@@ -3088,8 +3117,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Clear persistent favorites from keychain
|
||||
FavoritesPersistenceService.shared.clearAllFavorites()
|
||||
|
||||
// Clear identity data from secure storage
|
||||
identityManager.clearAllIdentityData()
|
||||
// Identity manager has cleared persisted identity data above
|
||||
|
||||
// Clear autocomplete state
|
||||
autocompleteSuggestions.removeAll()
|
||||
@@ -3119,6 +3147,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate {
|
||||
// Disconnect from all peers and clear persistent identity
|
||||
// This will force creation of a new identity (new fingerprint) on next launch
|
||||
meshService.emergencyDisconnectAll()
|
||||
if let bleService = meshService as? BLEService {
|
||||
bleService.resetIdentityForPanic(currentNickname: nickname)
|
||||
}
|
||||
|
||||
// No need to force UserDefaults synchronization
|
||||
|
||||
|
||||
@@ -462,7 +462,12 @@ struct ContentView: View {
|
||||
selectedMessageSender = viewModel.messages.last(where: { $0.senderPeerID == peerID && $0.sender != "system" })?.sender
|
||||
}
|
||||
}
|
||||
showMessageActions = true
|
||||
if viewModel.isSelfSender(peerID: selectedMessageSenderID, displayName: selectedMessageSender) {
|
||||
selectedMessageSender = nil
|
||||
selectedMessageSenderID = nil
|
||||
} else {
|
||||
showMessageActions = true
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
guard url.scheme == "bitchat", url.host == "geohash" else { return }
|
||||
|
||||
Reference in New Issue
Block a user