mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:45:20 +00:00
Add interactive logo with info/panic modes and battery optimization
- Single tap logo shows comprehensive app info and features - Triple tap logo triggers panic mode (instant data wipe) - Implement adaptive Bluetooth scanning based on battery level - Remove hardcoded development team ID for security - Fix store-and-forward description (12h for all, indefinite for favorites) - Add technical debt remediation plan - Fix macOS compatibility issues with navigation
This commit is contained in:
@@ -0,0 +1,148 @@
|
|||||||
|
# Bitchat Technical Debt Remediation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This document outlines the technical debt in the bitchat project and provides a prioritized plan for addressing it.
|
||||||
|
|
||||||
|
## Priority 1: Critical Security & Stability (1-2 weeks)
|
||||||
|
|
||||||
|
### 1.1 Thread Safety Issues
|
||||||
|
**Problem**: Potential race conditions in message processing and peer management
|
||||||
|
**Solution**:
|
||||||
|
- Audit all concurrent access to shared state
|
||||||
|
- Add proper synchronization to message queues
|
||||||
|
- Use actor pattern for BluetoothMeshService
|
||||||
|
- Add thread sanitizer to debug builds
|
||||||
|
|
||||||
|
### 1.2 Error Handling
|
||||||
|
**Problem**: Many errors are silently logged without proper recovery
|
||||||
|
**Solution**:
|
||||||
|
- Implement proper error propagation
|
||||||
|
- Add user-visible error states
|
||||||
|
- Create recovery mechanisms for common failures
|
||||||
|
- Add crash reporting for TestFlight
|
||||||
|
|
||||||
|
### 1.3 Memory Management
|
||||||
|
**Problem**: Fragment cleanup could leak memory, no proper cleanup on errors
|
||||||
|
**Solution**:
|
||||||
|
- Add fragment timeout cleanup
|
||||||
|
- Implement proper weak references in closures
|
||||||
|
- Add memory pressure handling
|
||||||
|
- Profile with Instruments
|
||||||
|
|
||||||
|
## Priority 2: Code Quality & Maintainability (2-3 weeks)
|
||||||
|
|
||||||
|
### 2.1 Service Refactoring
|
||||||
|
**Problem**: BluetoothMeshService is 1600+ lines, doing too much
|
||||||
|
**Solution**:
|
||||||
|
- Extract message handling into MessageProcessor
|
||||||
|
- Extract peer management into PeerManager
|
||||||
|
- Extract fragment handling into FragmentManager
|
||||||
|
- Create proper dependency injection
|
||||||
|
|
||||||
|
### 2.2 Testing Infrastructure
|
||||||
|
**Problem**: No unit tests, making refactoring risky
|
||||||
|
**Solution**:
|
||||||
|
- Add XCTest target
|
||||||
|
- Create mock implementations for Bluetooth
|
||||||
|
- Test encryption service thoroughly
|
||||||
|
- Add integration tests for message flow
|
||||||
|
- Aim for 70% code coverage
|
||||||
|
|
||||||
|
### 2.3 Documentation
|
||||||
|
**Problem**: Limited code comments, no architecture docs
|
||||||
|
**Solution**:
|
||||||
|
- Add comprehensive header comments
|
||||||
|
- Document protocol specifications
|
||||||
|
- Create architecture diagrams
|
||||||
|
- Add inline documentation for complex logic
|
||||||
|
|
||||||
|
## Priority 3: Performance & UX (3-4 weeks)
|
||||||
|
|
||||||
|
### 3.1 Message Deduplication
|
||||||
|
**Problem**: Simple Set-based deduplication can grow unbounded
|
||||||
|
**Solution**:
|
||||||
|
- Implement LRU cache for message IDs
|
||||||
|
- Add time-based expiration
|
||||||
|
- Consider bloom filters for efficiency
|
||||||
|
|
||||||
|
### 3.2 Connection Management
|
||||||
|
**Problem**: No connection pooling or retry logic
|
||||||
|
**Solution**:
|
||||||
|
- Implement connection state machine
|
||||||
|
- Add exponential backoff for retries
|
||||||
|
- Pool peripheral connections
|
||||||
|
- Add connection quality metrics
|
||||||
|
|
||||||
|
### 3.3 UI Responsiveness
|
||||||
|
**Problem**: Heavy operations on main thread
|
||||||
|
**Solution**:
|
||||||
|
- Move encryption to background queues
|
||||||
|
- Add loading states for operations
|
||||||
|
- Implement message pagination
|
||||||
|
- Add pull-to-refresh
|
||||||
|
|
||||||
|
## Priority 4: Feature Enhancements (4-6 weeks)
|
||||||
|
|
||||||
|
### 4.1 Message Persistence (Optional)
|
||||||
|
**Problem**: All messages ephemeral, no history
|
||||||
|
**Solution**:
|
||||||
|
- Add encrypted SQLite storage
|
||||||
|
- Implement configurable retention
|
||||||
|
- Add search functionality
|
||||||
|
- Maintain privacy-first approach
|
||||||
|
|
||||||
|
### 4.2 Protocol Improvements
|
||||||
|
**Problem**: No versioning, hard to upgrade
|
||||||
|
**Solution**:
|
||||||
|
- Add protocol version negotiation
|
||||||
|
- Implement capability discovery
|
||||||
|
- Plan migration strategy
|
||||||
|
- Add feature flags
|
||||||
|
|
||||||
|
### 4.3 Network Visualization
|
||||||
|
**Problem**: Users don't understand mesh topology
|
||||||
|
**Solution**:
|
||||||
|
- Add network graph view
|
||||||
|
- Show message routing paths
|
||||||
|
- Display peer connection quality
|
||||||
|
- Add statistics dashboard
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Phase 1: Foundation (Weeks 1-2)
|
||||||
|
1. Set up testing infrastructure
|
||||||
|
2. Add thread sanitizer and memory profiler
|
||||||
|
3. Begin service refactoring
|
||||||
|
4. Fix critical thread safety issues
|
||||||
|
|
||||||
|
### Phase 2: Stability (Weeks 3-4)
|
||||||
|
1. Complete service refactoring
|
||||||
|
2. Add comprehensive error handling
|
||||||
|
3. Implement proper memory management
|
||||||
|
4. Add initial test coverage
|
||||||
|
|
||||||
|
### Phase 3: Quality (Weeks 5-6)
|
||||||
|
1. Add documentation
|
||||||
|
2. Improve connection management
|
||||||
|
3. Optimize performance bottlenecks
|
||||||
|
4. Enhance UI responsiveness
|
||||||
|
|
||||||
|
### Phase 4: Enhancement (Weeks 7-10)
|
||||||
|
1. Add optional persistence
|
||||||
|
2. Implement protocol versioning
|
||||||
|
3. Build network visualization
|
||||||
|
4. Polish for production
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
- Zero crashes in TestFlight
|
||||||
|
- 70% test coverage
|
||||||
|
- Sub-100ms message processing
|
||||||
|
- < 5% battery drain per hour
|
||||||
|
- Clean architecture with < 300 lines per class
|
||||||
|
|
||||||
|
## Risk Mitigation
|
||||||
|
- Keep all changes backwards compatible initially
|
||||||
|
- Add feature flags for risky changes
|
||||||
|
- Extensive TestFlight testing between phases
|
||||||
|
- Maintain current security properties
|
||||||
|
- Regular security audits of changes
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 63;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -20,6 +20,8 @@
|
|||||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
||||||
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
||||||
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
|
||||||
|
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
||||||
|
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */; };
|
||||||
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
||||||
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
||||||
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
||||||
@@ -32,8 +34,9 @@
|
|||||||
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
3448F84BF86A42A3CC4A9379 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
||||||
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
||||||
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
||||||
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; };
|
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; };
|
||||||
@@ -85,6 +88,7 @@
|
|||||||
A55126E93155456CAA8D6656 /* Views */ = {
|
A55126E93155456CAA8D6656 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
@@ -158,11 +162,9 @@
|
|||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1430;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
0576A29205865664C0937536 = {
|
0576A29205865664C0937536 = {
|
||||||
DevelopmentTeam = L3N5LHJD5Y;
|
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
AF077EA0474EDEDE2C72716C = {
|
AF077EA0474EDEDE2C72716C = {
|
||||||
DevelopmentTeam = L3N5LHJD5Y;
|
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -177,7 +179,6 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 18198ED912AAF495D8AF7763;
|
mainGroup = 18198ED912AAF495D8AF7763;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
preferredProjectObjectVersion = 54;
|
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
@@ -211,6 +212,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
|
||||||
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
|
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
|
||||||
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
|
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
|
||||||
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
|
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
|
||||||
@@ -226,6 +228,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
|
||||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
|
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
|
||||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
|
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
|
||||||
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
|
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
|
||||||
@@ -249,6 +252,7 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
@@ -275,6 +279,7 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
@@ -353,7 +358,6 @@
|
|||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
@@ -437,7 +441,6 @@
|
|||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
|||||||
@@ -94,5 +94,12 @@ protocol BitchatDelegate: AnyObject {
|
|||||||
func didUpdatePeerList(_ peers: [String])
|
func didUpdatePeerList(_ peers: [String])
|
||||||
|
|
||||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||||
@objc optional func isFavorite(fingerprint: String) -> Bool
|
func isFavorite(fingerprint: String) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide default implementation to make it effectively optional
|
||||||
|
extension BitchatDelegate {
|
||||||
|
func isFavorite(fingerprint: String) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import Combine
|
|||||||
import CryptoKit
|
import CryptoKit
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import IOKit.ps
|
||||||
#else
|
#else
|
||||||
import UIKit
|
import UIKit
|
||||||
#endif
|
#endif
|
||||||
@@ -58,9 +59,11 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Battery and range optimizations
|
// Battery and range optimizations
|
||||||
private var scanDutyCycleTimer: Timer?
|
private var scanDutyCycleTimer: Timer?
|
||||||
private var isActivelyScanning = true
|
private var isActivelyScanning = true
|
||||||
private let activeScanDuration: TimeInterval = 2.0 // Scan actively for 2 seconds
|
private var activeScanDuration: TimeInterval = 2.0 // Scan actively for 2 seconds - will be adjusted based on battery
|
||||||
private let scanPauseDuration: TimeInterval = 3.0 // Pause for 3 seconds
|
private var scanPauseDuration: TimeInterval = 3.0 // Pause for 3 seconds - will be adjusted based on battery
|
||||||
private var lastRSSIUpdate: [String: Date] = [:] // Throttle RSSI updates
|
private var lastRSSIUpdate: [String: Date] = [:] // Throttle RSSI updates
|
||||||
|
private var batteryMonitorTimer: Timer?
|
||||||
|
private var currentBatteryLevel: Float = 1.0 // Default to full battery
|
||||||
|
|
||||||
// Fragment handling
|
// Fragment handling
|
||||||
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
||||||
@@ -226,6 +229,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
options: scanOptions
|
options: scanOptions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Update scan parameters based on battery before starting
|
||||||
|
updateScanParametersForBattery()
|
||||||
|
|
||||||
// Implement scan duty cycling for battery efficiency
|
// Implement scan duty cycling for battery efficiency
|
||||||
scheduleScanDutyCycle()
|
scheduleScanDutyCycle()
|
||||||
}
|
}
|
||||||
@@ -524,7 +530,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Check if recipient is a favorite via their public key fingerprint
|
// Check if recipient is a favorite via their public key fingerprint
|
||||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
||||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||||
isForFavorite = self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false
|
isForFavorite = self.delegate?.isFavorite(fingerprint: fingerprint) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,10 +618,17 @@ class BluetoothMeshService: NSObject {
|
|||||||
peripheral.state == .connected else { return }
|
peripheral.state == .connected else { return }
|
||||||
|
|
||||||
// Create a new packet with fresh timestamp
|
// Create a new packet with fresh timestamp
|
||||||
var packet = storedMessage.packet
|
let updatedPacket = BitchatPacket(
|
||||||
packet.timestamp = UInt64(Date().timeIntervalSince1970)
|
type: storedMessage.packet.type,
|
||||||
|
senderID: storedMessage.packet.senderID,
|
||||||
|
recipientID: storedMessage.packet.recipientID,
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970),
|
||||||
|
payload: storedMessage.packet.payload,
|
||||||
|
signature: storedMessage.packet.signature,
|
||||||
|
ttl: storedMessage.packet.ttl
|
||||||
|
)
|
||||||
|
|
||||||
if let data = packet.toBinaryData() {
|
if let data = updatedPacket.toBinaryData() {
|
||||||
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
||||||
print("[CACHE] Sent cached message \(index + 1)/\(messagesToSend.count) to \(peerID)")
|
print("[CACHE] Sent cached message \(index + 1)/\(messagesToSend.count) to \(peerID)")
|
||||||
}
|
}
|
||||||
@@ -882,7 +895,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Check if this is a favorite using their public key fingerprint
|
// Check if this is a favorite using their public key fingerprint
|
||||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(senderID) {
|
if let publicKeyData = self.encryptionService.getPeerIdentityKey(senderID) {
|
||||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||||
if self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false {
|
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||||
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
|
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
|
||||||
|
|
||||||
// Send any cached messages for this favorite
|
// Send any cached messages for this favorite
|
||||||
@@ -1014,7 +1027,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Check if this message is for an offline favorite and cache it
|
// Check if this message is for an offline favorite and cache it
|
||||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
||||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||||
if self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false {
|
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||||
// This is for a favorite peer - cache it even if they're offline
|
// This is for a favorite peer - cache it even if they're offline
|
||||||
print("[CACHE] Caching relayed message for offline favorite: \(recipientIDString)")
|
print("[CACHE] Caching relayed message for offline favorite: \(recipientIDString)")
|
||||||
self.cacheMessage(relayPacket, messageID: messageID)
|
self.cacheMessage(relayPacket, messageID: messageID)
|
||||||
@@ -1615,4 +1628,91 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
startAdvertising()
|
startAdvertising()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Battery Monitoring
|
||||||
|
|
||||||
|
private func startBatteryMonitoring() {
|
||||||
|
// Update battery level immediately
|
||||||
|
updateBatteryLevel()
|
||||||
|
|
||||||
|
// Monitor battery level every 30 seconds
|
||||||
|
batteryMonitorTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||||
|
self?.updateBatteryLevel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateBatteryLevel() {
|
||||||
|
#if os(iOS)
|
||||||
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||||
|
currentBatteryLevel = UIDevice.current.batteryLevel
|
||||||
|
|
||||||
|
// Battery level is -1 when unknown (e.g., in simulator)
|
||||||
|
if currentBatteryLevel < 0 {
|
||||||
|
currentBatteryLevel = 1.0 // Assume full battery when unknown
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// macOS battery monitoring
|
||||||
|
if let batteryInfo = getMacOSBatteryInfo() {
|
||||||
|
currentBatteryLevel = batteryInfo
|
||||||
|
} else {
|
||||||
|
currentBatteryLevel = 1.0 // Assume full battery when unknown
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
print("[BATTERY] Current battery level: \(Int(currentBatteryLevel * 100))%")
|
||||||
|
updateScanParametersForBattery()
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
private func getMacOSBatteryInfo() -> Float? {
|
||||||
|
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
||||||
|
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
||||||
|
|
||||||
|
for source in sources {
|
||||||
|
if let description = IOPSGetPowerSourceDescription(snapshot, source).takeUnretainedValue() as? [String: Any] {
|
||||||
|
if let currentCapacity = description[kIOPSCurrentCapacityKey] as? Int,
|
||||||
|
let maxCapacity = description[kIOPSMaxCapacityKey] as? Int {
|
||||||
|
return Float(currentCapacity) / Float(maxCapacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private func updateScanParametersForBattery() {
|
||||||
|
// Adaptive scanning based on battery level
|
||||||
|
// High battery (80%+): Normal scanning
|
||||||
|
// Medium battery (40-80%): Moderate power saving
|
||||||
|
// Low battery (20-40%): Aggressive power saving
|
||||||
|
// Critical battery (<20%): Maximum power saving
|
||||||
|
|
||||||
|
if currentBatteryLevel > 0.8 {
|
||||||
|
// High battery: Normal operation
|
||||||
|
activeScanDuration = 2.0
|
||||||
|
scanPauseDuration = 3.0
|
||||||
|
print("[BATTERY] High battery mode: normal scanning")
|
||||||
|
} else if currentBatteryLevel > 0.4 {
|
||||||
|
// Medium battery: Moderate power saving
|
||||||
|
activeScanDuration = 1.5
|
||||||
|
scanPauseDuration = 4.5
|
||||||
|
print("[BATTERY] Medium battery mode: moderate power saving")
|
||||||
|
} else if currentBatteryLevel > 0.2 {
|
||||||
|
// Low battery: Aggressive power saving
|
||||||
|
activeScanDuration = 1.0
|
||||||
|
scanPauseDuration = 8.0
|
||||||
|
print("[BATTERY] Low battery mode: aggressive power saving")
|
||||||
|
} else {
|
||||||
|
// Critical battery: Maximum power saving
|
||||||
|
activeScanDuration = 0.5
|
||||||
|
scanPauseDuration = 15.0
|
||||||
|
print("[BATTERY] Critical battery mode: maximum power saving")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're currently in a duty cycle, restart it with new parameters
|
||||||
|
if scanDutyCycleTimer != nil {
|
||||||
|
scanDutyCycleTimer?.invalidate()
|
||||||
|
scheduleScanDutyCycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AppInfoView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
private var backgroundColor: Color {
|
||||||
|
colorScheme == .dark ? Color.black : Color.white
|
||||||
|
}
|
||||||
|
|
||||||
|
private var textColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var secondaryTextColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
#if os(macOS)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Custom header for macOS
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(backgroundColor.opacity(0.95))
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 24) {
|
||||||
|
// Header
|
||||||
|
VStack(alignment: .center, spacing: 8) {
|
||||||
|
Text("bitchat*")
|
||||||
|
.font(.system(size: 32, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text("secure mesh chat")
|
||||||
|
.font(.system(size: 16, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
// Features
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Features")
|
||||||
|
|
||||||
|
FeatureRow(icon: "wifi.slash", title: "Offline Communication",
|
||||||
|
description: "Works without internet using Bluetooth mesh networking")
|
||||||
|
|
||||||
|
FeatureRow(icon: "lock.shield", title: "End-to-End Encryption",
|
||||||
|
description: "All messages encrypted with Curve25519 + AES-GCM")
|
||||||
|
|
||||||
|
FeatureRow(icon: "antenna.radiowaves.left.and.right", title: "Extended Range",
|
||||||
|
description: "Messages relay through peers, reaching 300m+")
|
||||||
|
|
||||||
|
FeatureRow(icon: "clock.arrow.circlepath", title: "Ephemeral Messages",
|
||||||
|
description: "Messages auto-delete after 5 minutes")
|
||||||
|
|
||||||
|
FeatureRow(icon: "star.fill", title: "Favorites System",
|
||||||
|
description: "Store-and-forward messages for favorites indefinitely")
|
||||||
|
|
||||||
|
FeatureRow(icon: "at", title: "Mentions",
|
||||||
|
description: "Use @nickname to notify specific users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Privacy
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Privacy")
|
||||||
|
|
||||||
|
FeatureRow(icon: "eye.slash", title: "No Tracking",
|
||||||
|
description: "No servers, accounts, or data collection")
|
||||||
|
|
||||||
|
FeatureRow(icon: "shuffle", title: "Ephemeral Identity",
|
||||||
|
description: "New peer ID generated each session")
|
||||||
|
|
||||||
|
FeatureRow(icon: "hand.raised.fill", title: "Panic Mode",
|
||||||
|
description: "Triple-tap logo to instantly clear all data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// How to Use
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("How to Use")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("• Set your nickname in the header")
|
||||||
|
Text("• Tap the people counter to see connected peers")
|
||||||
|
Text("• Tap a peer to start a private chat")
|
||||||
|
Text("• Use @nickname to mention someone")
|
||||||
|
Text("• Triple-tap the logo for panic mode")
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Technical Details
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Technical Details")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Protocol: Custom binary over BLE")
|
||||||
|
Text("Encryption: Curve25519 + AES-256-GCM")
|
||||||
|
Text("Range: ~100m direct, 300m+ with relay")
|
||||||
|
Text("Store & Forward: 12h for all, ∞ for favorites")
|
||||||
|
Text("Battery: Adaptive scanning based on level")
|
||||||
|
Text("Platform: Universal (iOS, iPadOS, macOS)")
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("Version 1.0.0")
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
}
|
||||||
|
.frame(width: 600, height: 700)
|
||||||
|
#else
|
||||||
|
NavigationView {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 24) {
|
||||||
|
// Header
|
||||||
|
VStack(alignment: .center, spacing: 8) {
|
||||||
|
Text("bitchat*")
|
||||||
|
.font(.system(size: 32, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text("secure mesh chat")
|
||||||
|
.font(.system(size: 16, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
// Features
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Features")
|
||||||
|
|
||||||
|
FeatureRow(icon: "wifi.slash", title: "Offline Communication",
|
||||||
|
description: "Works without internet using Bluetooth mesh networking")
|
||||||
|
|
||||||
|
FeatureRow(icon: "lock.shield", title: "End-to-End Encryption",
|
||||||
|
description: "All messages encrypted with Curve25519 + AES-GCM")
|
||||||
|
|
||||||
|
FeatureRow(icon: "antenna.radiowaves.left.and.right", title: "Extended Range",
|
||||||
|
description: "Messages relay through peers, reaching 300m+")
|
||||||
|
|
||||||
|
FeatureRow(icon: "clock.arrow.circlepath", title: "Ephemeral Messages",
|
||||||
|
description: "Messages auto-delete after 5 minutes")
|
||||||
|
|
||||||
|
FeatureRow(icon: "star.fill", title: "Favorites System",
|
||||||
|
description: "Store-and-forward messages for favorites indefinitely")
|
||||||
|
|
||||||
|
FeatureRow(icon: "at", title: "Mentions",
|
||||||
|
description: "Use @nickname to notify specific users")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Privacy
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Privacy")
|
||||||
|
|
||||||
|
FeatureRow(icon: "eye.slash", title: "No Tracking",
|
||||||
|
description: "No servers, accounts, or data collection")
|
||||||
|
|
||||||
|
FeatureRow(icon: "shuffle", title: "Ephemeral Identity",
|
||||||
|
description: "New peer ID generated each session")
|
||||||
|
|
||||||
|
FeatureRow(icon: "hand.raised.fill", title: "Panic Mode",
|
||||||
|
description: "Triple-tap logo to instantly clear all data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// How to Use
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("How to Use")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("• Set your nickname in the header")
|
||||||
|
Text("• Tap the people counter to see connected peers")
|
||||||
|
Text("• Tap a peer to start a private chat")
|
||||||
|
Text("• Use @nickname to mention someone")
|
||||||
|
Text("• Triple-tap the logo for panic mode")
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Technical Details
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
SectionHeader("Technical Details")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Protocol: Custom binary over BLE")
|
||||||
|
Text("Encryption: Curve25519 + AES-256-GCM")
|
||||||
|
Text("Range: ~100m direct, 300m+ with relay")
|
||||||
|
Text("Store & Forward: 12h for all, ∞ for favorites")
|
||||||
|
Text("Battery: Adaptive scanning based on level")
|
||||||
|
Text("Platform: Universal (iOS, iPadOS, macOS)")
|
||||||
|
}
|
||||||
|
.font(.system(size: 14, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("Version 1.0.0")
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(backgroundColor)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button("Done") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SectionHeader: View {
|
||||||
|
let title: String
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
private var textColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ title: String) {
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(title.uppercased())
|
||||||
|
.font(.system(size: 16, weight: .bold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.padding(.top, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FeatureRow: View {
|
||||||
|
let icon: String
|
||||||
|
let title: String
|
||||||
|
let description: String
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
private var textColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var secondaryTextColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .top, spacing: 12) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.frame(width: 30)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(title)
|
||||||
|
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text(description)
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(secondaryTextColor)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
AppInfoView()
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ struct ContentView: View {
|
|||||||
@State private var showPeerList = false
|
@State private var showPeerList = false
|
||||||
@State private var showSidebar = false
|
@State private var showSidebar = false
|
||||||
@State private var sidebarDragOffset: CGFloat = 0
|
@State private var sidebarDragOffset: CGFloat = 0
|
||||||
|
@State private var showAppInfo = false
|
||||||
|
|
||||||
private var backgroundColor: Color {
|
private var backgroundColor: Color {
|
||||||
colorScheme == .dark ? Color.black : Color.white
|
colorScheme == .dark ? Color.black : Color.white
|
||||||
@@ -132,6 +133,9 @@ struct ContentView: View {
|
|||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 600, minHeight: 400)
|
.frame(minWidth: 600, minHeight: 400)
|
||||||
#endif
|
#endif
|
||||||
|
.sheet(isPresented: $showAppInfo) {
|
||||||
|
AppInfoView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var headerView: some View {
|
private var headerView: some View {
|
||||||
@@ -180,6 +184,10 @@ struct ContentView: View {
|
|||||||
// PANIC: Triple-tap to clear all data
|
// PANIC: Triple-tap to clear all data
|
||||||
viewModel.panicClearAllData()
|
viewModel.panicClearAllData()
|
||||||
}
|
}
|
||||||
|
.onTapGesture(count: 1) {
|
||||||
|
// Single tap for app info
|
||||||
|
showAppInfo = true
|
||||||
|
}
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text("@")
|
Text("@")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ options:
|
|||||||
settings:
|
settings:
|
||||||
MARKETING_VERSION: 1.0.0
|
MARKETING_VERSION: 1.0.0
|
||||||
CURRENT_PROJECT_VERSION: 1
|
CURRENT_PROJECT_VERSION: 1
|
||||||
DEVELOPMENT_TEAM: "L3N5LHJD5Y"
|
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
bitchat:
|
bitchat:
|
||||||
|
|||||||
Reference in New Issue
Block a user