mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 04:05: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;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 63;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -20,6 +20,8 @@
|
||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
||||
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.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 */; };
|
||||
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -85,6 +88,7 @@
|
||||
A55126E93155456CAA8D6656 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
|
||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
@@ -158,11 +162,9 @@
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
0576A29205865664C0937536 = {
|
||||
DevelopmentTeam = L3N5LHJD5Y;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
AF077EA0474EDEDE2C72716C = {
|
||||
DevelopmentTeam = L3N5LHJD5Y;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
};
|
||||
@@ -177,7 +179,6 @@
|
||||
);
|
||||
mainGroup = 18198ED912AAF495D8AF7763;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 54;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
@@ -211,6 +212,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
AD11E46940D742AEAF547EB2 /* AppInfoView.swift in Sources */,
|
||||
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */,
|
||||
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
|
||||
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
|
||||
@@ -226,6 +228,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
ABAF130D88561F4A646F0430 /* AppInfoView.swift in Sources */,
|
||||
F455F011B3B648ADA233F998 /* BinaryProtocol.swift in Sources */,
|
||||
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
|
||||
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
|
||||
@@ -249,6 +252,7 @@
|
||||
CODE_SIGNING_REQUIRED = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = bitchat/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -275,6 +279,7 @@
|
||||
CODE_SIGNING_REQUIRED = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = bitchat/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
@@ -353,7 +358,6 @@
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -437,7 +441,6 @@
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
||||
@@ -94,5 +94,12 @@ protocol BitchatDelegate: AnyObject {
|
||||
func didUpdatePeerList(_ peers: [String])
|
||||
|
||||
// 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
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
import IOKit.ps
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
@@ -58,9 +59,11 @@ class BluetoothMeshService: NSObject {
|
||||
// Battery and range optimizations
|
||||
private var scanDutyCycleTimer: Timer?
|
||||
private var isActivelyScanning = true
|
||||
private let activeScanDuration: TimeInterval = 2.0 // Scan actively for 2 seconds
|
||||
private let scanPauseDuration: TimeInterval = 3.0 // Pause for 3 seconds
|
||||
private var activeScanDuration: TimeInterval = 2.0 // Scan actively for 2 seconds - will be adjusted based on battery
|
||||
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 batteryMonitorTimer: Timer?
|
||||
private var currentBatteryLevel: Float = 1.0 // Default to full battery
|
||||
|
||||
// Fragment handling
|
||||
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
||||
@@ -226,6 +229,9 @@ class BluetoothMeshService: NSObject {
|
||||
options: scanOptions
|
||||
)
|
||||
|
||||
// Update scan parameters based on battery before starting
|
||||
updateScanParametersForBattery()
|
||||
|
||||
// Implement scan duty cycling for battery efficiency
|
||||
scheduleScanDutyCycle()
|
||||
}
|
||||
@@ -524,7 +530,7 @@ class BluetoothMeshService: NSObject {
|
||||
// Check if recipient is a favorite via their public key fingerprint
|
||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
||||
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 }
|
||||
|
||||
// Create a new packet with fresh timestamp
|
||||
var packet = storedMessage.packet
|
||||
packet.timestamp = UInt64(Date().timeIntervalSince1970)
|
||||
let updatedPacket = BitchatPacket(
|
||||
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)
|
||||
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
|
||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(senderID) {
|
||||
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
||||
if self.delegate?.isFavorite?(fingerprint: fingerprint) ?? false {
|
||||
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
||||
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
|
||||
|
||||
// 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
|
||||
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
||||
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
|
||||
print("[CACHE] Caching relayed message for offline favorite: \(recipientIDString)")
|
||||
self.cacheMessage(relayPacket, messageID: messageID)
|
||||
@@ -1615,4 +1628,91 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
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 showSidebar = false
|
||||
@State private var sidebarDragOffset: CGFloat = 0
|
||||
@State private var showAppInfo = false
|
||||
|
||||
private var backgroundColor: Color {
|
||||
colorScheme == .dark ? Color.black : Color.white
|
||||
@@ -132,6 +133,9 @@ struct ContentView: View {
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 600, minHeight: 400)
|
||||
#endif
|
||||
.sheet(isPresented: $showAppInfo) {
|
||||
AppInfoView()
|
||||
}
|
||||
}
|
||||
|
||||
private var headerView: some View {
|
||||
@@ -180,6 +184,10 @@ struct ContentView: View {
|
||||
// PANIC: Triple-tap to clear all data
|
||||
viewModel.panicClearAllData()
|
||||
}
|
||||
.onTapGesture(count: 1) {
|
||||
// Single tap for app info
|
||||
showAppInfo = true
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("@")
|
||||
|
||||
@@ -9,7 +9,6 @@ options:
|
||||
settings:
|
||||
MARKETING_VERSION: 1.0.0
|
||||
CURRENT_PROJECT_VERSION: 1
|
||||
DEVELOPMENT_TEAM: "L3N5LHJD5Y"
|
||||
|
||||
targets:
|
||||
bitchat:
|
||||
|
||||
Reference in New Issue
Block a user