Merge branch 'main' into routingOpt
@@ -8,7 +8,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Permission onboarding screen UX: removed "Exit App" button and fixed "Grant Permissions" button positioning to always be visible
|
||||||
|
|
||||||
|
## [0.6]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Channel password management with `/pass` command for channel owners
|
||||||
|
- Monochrome/themed launcher icon for Android 12+ dynamic theming support
|
||||||
|
- Unit tests package with initial testing infrastructure
|
||||||
|
- Production build optimization with code minification and shrinking
|
||||||
|
- Native back gesture/button handling for all app views
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Favorite peer functionality completely restored and improved
|
||||||
|
- Enhanced favorite system with fallback mechanism for peers without key exchange
|
||||||
|
- Fixed UI state updates for favorite stars in both header and sidebar
|
||||||
|
- Improved favorite persistence across app sessions
|
||||||
- `/w` command now displays user nicknames instead of peer IDs
|
- `/w` command now displays user nicknames instead of peer IDs
|
||||||
|
- Button styling and layout improvements across the app
|
||||||
|
- Enhanced back button positioning and styling
|
||||||
|
- Improved private chat and channel header button layouts
|
||||||
|
- Fixed button padding and alignment issues
|
||||||
|
- Color scheme consistency updates
|
||||||
|
- Updated orange color throughout the app to match iOS version
|
||||||
|
- Consistent color usage for private messages and UI elements
|
||||||
|
- App startup reliability improvements
|
||||||
|
- Better initialization sequence handling
|
||||||
|
- Fixed null pointer exceptions during startup
|
||||||
|
- Enhanced error handling and logging
|
||||||
|
- Input field styling and behavior improvements
|
||||||
|
- Sidebar user interaction enhancements
|
||||||
|
- Permission explanation screen layout fixes with proper vertical padding
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated GitHub organization references in project files
|
||||||
|
- Improved README documentation with updated clone URLs
|
||||||
|
- Enhanced logging throughout the application for better debugging
|
||||||
|
|
||||||
## [0.5.1] - 2025-07-10
|
## [0.5.1] - 2025-07-10
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
# ChatScreen.kt Refactoring Plan
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
- Single file: `ChatScreen.kt` (~1,100+ lines)
|
|
||||||
- Multiple UI responsibilities mixed together
|
|
||||||
- Hard to maintain and test individual components
|
|
||||||
|
|
||||||
## Proposed Component Structure
|
|
||||||
|
|
||||||
### 1. Main Screen (ChatScreen.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Main layout orchestration
|
|
||||||
- State management delegation
|
|
||||||
- Window insets handling
|
|
||||||
- Component coordination
|
|
||||||
|
|
||||||
### 2. Header Components (ChatHeader.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- TopAppBar with different states (main, private, channel)
|
|
||||||
- Nickname editor
|
|
||||||
- Peer counter with status indicators
|
|
||||||
- Navigation controls
|
|
||||||
|
|
||||||
### 3. Message Components (MessageComponents.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- MessagesList composable
|
|
||||||
- MessageItem with formatting
|
|
||||||
- Message text parsing and styling
|
|
||||||
- Delivery status indicators
|
|
||||||
- RSSI-based coloring
|
|
||||||
|
|
||||||
### 4. Input Components (InputComponents.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- MessageInput with different modes
|
|
||||||
- Command suggestions box
|
|
||||||
- Command suggestion items
|
|
||||||
- Input validation and handling
|
|
||||||
|
|
||||||
### 5. Sidebar Components (SidebarComponents.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- SidebarOverlay with navigation
|
|
||||||
- ChannelsSection for channel management
|
|
||||||
- PeopleSection for peer list
|
|
||||||
- Sidebar state management
|
|
||||||
|
|
||||||
### 6. Dialog Components (DialogComponents.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Password prompt dialog
|
|
||||||
- App info dialog
|
|
||||||
- Other modal dialogs
|
|
||||||
|
|
||||||
### 7. UI Utils (ChatUIUtils.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- RSSI color mapping
|
|
||||||
- Text formatting utilities
|
|
||||||
- Common styling constants
|
|
||||||
- Helper functions
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
- Each file has a single, clear responsibility
|
|
||||||
- Components are easier to test in isolation
|
|
||||||
- Better code organization and navigation
|
|
||||||
- Simplified debugging
|
|
||||||
- Easier to add new UI features
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# BluetoothMeshService Refactoring - Progress Report
|
|
||||||
|
|
||||||
## ✅ COMPLETED: Extracted Components (All compile successfully)
|
|
||||||
|
|
||||||
### 1. PeerManager.kt (161 lines)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Active peer tracking and lifecycle management
|
|
||||||
- Peer nickname management and stale peer cleanup
|
|
||||||
- RSSI tracking and peer list updates
|
|
||||||
- **Interface:** `PeerManagerDelegate`
|
|
||||||
|
|
||||||
### 2. FragmentManager.kt (194 lines)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Message fragmentation for large messages (>500 bytes)
|
|
||||||
- Fragment reassembly and cleanup
|
|
||||||
- Fragment timeout management (30 seconds)
|
|
||||||
- **Interface:** `FragmentManagerDelegate`
|
|
||||||
|
|
||||||
### 3. SecurityManager.kt (236 lines)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Duplicate detection and replay attack protection
|
|
||||||
- Key exchange handling and validation
|
|
||||||
- Message encryption/decryption operations
|
|
||||||
- Packet signature verification
|
|
||||||
- **Interface:** `SecurityManagerDelegate`
|
|
||||||
|
|
||||||
### 4. StoreForwardManager.kt (295 lines)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Message caching for offline peers (12 hours regular, unlimited favorites)
|
|
||||||
- Store-and-forward delivery when peers come online
|
|
||||||
- Cache cleanup and management
|
|
||||||
- **Interface:** `StoreForwardManagerDelegate`
|
|
||||||
|
|
||||||
### 5. MessageHandler.kt (284 lines)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Processing different message types (ANNOUNCE, MESSAGE, LEAVE, etc.)
|
|
||||||
- Broadcast vs private message handling
|
|
||||||
- Message relay logic with adaptive probability
|
|
||||||
- Delivery acknowledgment sending
|
|
||||||
- **Interface:** `MessageHandlerDelegate`
|
|
||||||
|
|
||||||
### 6. BluetoothConnectionManager.kt (611 lines)
|
|
||||||
**Responsibilities:**
|
|
||||||
- BLE advertising and scanning
|
|
||||||
- GATT server/client setup and management
|
|
||||||
- Device connection tracking (both server and client modes)
|
|
||||||
- Packet broadcasting to all connected devices
|
|
||||||
- **Interface:** `BluetoothConnectionManagerDelegate`
|
|
||||||
|
|
||||||
## 📊 Size Reduction Analysis
|
|
||||||
|
|
||||||
| Component | Lines | Responsibility |
|
|
||||||
|-----------|--------|----------------|
|
|
||||||
| **PeerManager** | 161 | Peer lifecycle & tracking |
|
|
||||||
| **FragmentManager** | 194 | Message fragmentation |
|
|
||||||
| **SecurityManager** | 236 | Security & encryption |
|
|
||||||
| **StoreForwardManager** | 295 | Offline message caching |
|
|
||||||
| **MessageHandler** | 284 | Message type processing |
|
|
||||||
| **BluetoothConnectionManager** | 611 | BLE connection management |
|
|
||||||
| **Original File** | ~1000+ | All responsibilities mixed |
|
|
||||||
|
|
||||||
**Total Extracted:** ~1781 lines (distributed across 6 focused files)
|
|
||||||
**Reduction Factor:** Original single file → 6 smaller, focused components
|
|
||||||
|
|
||||||
## 🏗️ Next Steps for Integration
|
|
||||||
|
|
||||||
### Phase 1: Refactor Existing BluetoothMeshService
|
|
||||||
1. **Update BluetoothMeshService.kt** to use the new components
|
|
||||||
2. **Wire up all delegate interfaces**
|
|
||||||
3. **Maintain exact same public API** so ChatViewModel doesn't change
|
|
||||||
4. **Test compilation and functionality**
|
|
||||||
|
|
||||||
### Phase 2: Integration Testing
|
|
||||||
1. **Verify all existing functionality works**
|
|
||||||
2. **Test key scenarios:**
|
|
||||||
- Peer discovery and connection
|
|
||||||
- Message sending/receiving
|
|
||||||
- Fragment handling
|
|
||||||
- Store-and-forward delivery
|
|
||||||
- Security validation
|
|
||||||
|
|
||||||
### Phase 3: Benefits Validation
|
|
||||||
1. **Easier unit testing** (each component can be tested independently)
|
|
||||||
2. **Better code maintainability** (clear separation of concerns)
|
|
||||||
3. **Improved debugging** (isolated component logs)
|
|
||||||
4. **Future extensibility** (easier to add new features)
|
|
||||||
|
|
||||||
## 🔧 Current Build Status
|
|
||||||
✅ **All 6 extracted components compile successfully**
|
|
||||||
✅ **No breaking changes to existing interfaces**
|
|
||||||
✅ **Original BluetoothMeshService.kt still intact**
|
|
||||||
✅ **Ready for integration phase**
|
|
||||||
|
|
||||||
## 💡 Key Design Decisions Made
|
|
||||||
|
|
||||||
1. **Delegate Pattern:** Each component uses a delegate interface for clean separation
|
|
||||||
2. **Coroutine Scope per Component:** Isolated lifecycle management
|
|
||||||
3. **Thread-Safe Collections:** Maintained from original implementation
|
|
||||||
4. **Same UUIDs and Constants:** No protocol changes
|
|
||||||
5. **Preserved iOS Compatibility:** All timing and logic matches iOS exactly
|
|
||||||
6. **Error Handling:** Maintained original defensive programming patterns
|
|
||||||
|
|
||||||
The refactoring successfully breaks down a monolithic 1000+ line service into 6 focused, maintainable components while preserving 100% compatibility with the iOS implementation.
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
# RSSI Color Change Fix - Implementation Summary
|
|
||||||
|
|
||||||
## Problem Identified
|
|
||||||
The username colors in the chat were not changing based on RSSI signal strength even though RSSI values were being logged as changing. Investigation revealed that:
|
|
||||||
|
|
||||||
1. **RSSI values were only captured once** during the initial BLE scan discovery
|
|
||||||
2. **No mechanism existed** to continuously update RSSI values from connected devices
|
|
||||||
3. **UI was not being notified** of RSSI changes to trigger recomposition
|
|
||||||
4. **Chat rendering was using stale RSSI values** that never changed after initial connection
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
The `BluetoothMeshService` was only recording RSSI during the scan phase (`handleScanResult`) but never updating it during the connection lifetime. Bluetooth GATT provides `readRemoteRssi()` for connected devices, but this wasn't being used.
|
|
||||||
|
|
||||||
## Solution Implemented
|
|
||||||
|
|
||||||
### 1. Device-to-Peer ID Mapping
|
|
||||||
- Added `deviceToPeerIDMapping` to link Bluetooth devices to peer IDs
|
|
||||||
- This allows RSSI updates to be associated with the correct peer ID
|
|
||||||
|
|
||||||
### 2. GATT RSSI Reading Callback
|
|
||||||
- Added `onReadRemoteRssi()` callback in the GATT client callback
|
|
||||||
- Updates `peerRSSI` map when new RSSI values are read
|
|
||||||
- Maps device addresses to peer IDs for proper tracking
|
|
||||||
|
|
||||||
### 3. Periodic RSSI Monitoring
|
|
||||||
- Added background coroutine that runs every 5 seconds
|
|
||||||
- Calls `readRemoteRssi()` on all active GATT connections
|
|
||||||
- Provides continuous RSSI updates during connection lifetime
|
|
||||||
|
|
||||||
### 4. UI Notification System
|
|
||||||
- Added `didUpdateRSSI()` delegate method to notify UI of RSSI changes
|
|
||||||
- When RSSI changes, the delegate is called to potentially trigger UI updates
|
|
||||||
- Chat screen automatically recomposes when RSSI values change
|
|
||||||
|
|
||||||
### 5. Key Exchange Enhancement
|
|
||||||
- Modified `handleKeyExchange()` to map devices to peer IDs
|
|
||||||
- Transfers any existing peripheral RSSI data to peer-based tracking
|
|
||||||
- Ensures proper RSSI association after peer identification
|
|
||||||
|
|
||||||
## Code Changes Made
|
|
||||||
|
|
||||||
### BluetoothMeshService.kt
|
|
||||||
1. **Added device mapping**: `deviceToPeerIDMapping` concurrent hash map
|
|
||||||
2. **RSSI callback**: `onReadRemoteRssi()` implementation in GATT callback
|
|
||||||
3. **Periodic monitoring**: `monitorRSSI()` function called every 5 seconds
|
|
||||||
4. **Key exchange update**: Links devices to peer IDs for RSSI tracking
|
|
||||||
5. **Delegate method**: `didUpdateRSSI()` interface method for UI notifications
|
|
||||||
|
|
||||||
### ChatViewModel.kt
|
|
||||||
1. **Delegate implementation**: Added `didUpdateRSSI()` method
|
|
||||||
2. **Logging**: Debug output when RSSI values change
|
|
||||||
|
|
||||||
### ChatScreen.kt
|
|
||||||
- **No changes needed** - existing `getRSSIColor(rssi)` function already works
|
|
||||||
- UI automatically recomposes when `meshService.getPeerRSSI()` returns updated values
|
|
||||||
|
|
||||||
## How It Works Now
|
|
||||||
|
|
||||||
1. **Initial Discovery**: RSSI captured during BLE scan (as before)
|
|
||||||
2. **Connection**: Device mapped to peer ID during key exchange
|
|
||||||
3. **Monitoring**: Every 5 seconds, `readRemoteRssi()` called on connected devices
|
|
||||||
4. **Update**: RSSI callback updates `peerRSSI` map with new values
|
|
||||||
5. **Notification**: Delegate notified of RSSI change
|
|
||||||
6. **Rendering**: Chat screen uses updated RSSI values for color calculation
|
|
||||||
7. **Recomposition**: UI automatically updates with new colors
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
- Username colors now change dynamically as users move closer/farther away
|
|
||||||
- Colors reflect real-time signal strength: green (strong) → yellow (medium) → red (weak)
|
|
||||||
- Updates occur every 5 seconds while devices are connected
|
|
||||||
- Your own username remains green regardless of signal strength
|
|
||||||
- Works for both client and server GATT connections
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
- Build successful with `./gradlew assembleDebug`
|
|
||||||
- No compilation errors
|
|
||||||
- All existing functionality preserved
|
|
||||||
- Ready for testing with actual devices
|
|
||||||
|
|
||||||
The fix addresses the core issue where RSSI values were static after connection. Now they continuously update, providing the dynamic color feedback based on signal strength that was originally intended.
|
|
||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.bitchat.android"
|
applicationId = "com.bitchat.android"
|
||||||
minSdk = libs.versions.minSdk.get().toInt()
|
minSdk = libs.versions.minSdk.get().toInt()
|
||||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0"
|
versionName = "0.6"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.addCallback
|
||||||
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.onboarding.*
|
import com.bitchat.android.onboarding.*
|
||||||
import com.bitchat.android.ui.ChatScreen
|
import com.bitchat.android.ui.ChatScreen
|
||||||
import com.bitchat.android.ui.ChatViewModel
|
import com.bitchat.android.ui.ChatViewModel
|
||||||
@@ -29,7 +33,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
private lateinit var permissionManager: PermissionManager
|
private lateinit var permissionManager: PermissionManager
|
||||||
private lateinit var onboardingCoordinator: OnboardingCoordinator
|
private lateinit var onboardingCoordinator: OnboardingCoordinator
|
||||||
private lateinit var bluetoothStatusManager: BluetoothStatusManager
|
private lateinit var bluetoothStatusManager: BluetoothStatusManager
|
||||||
private val chatViewModel: ChatViewModel by viewModels()
|
|
||||||
|
// Core mesh service - managed at app level
|
||||||
|
private lateinit var meshService: BluetoothMeshService
|
||||||
|
private val chatViewModel: ChatViewModel by viewModels {
|
||||||
|
object : ViewModelProvider.Factory {
|
||||||
|
override fun <T : androidx.lifecycle.ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return ChatViewModel(application, meshService) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UI state for onboarding flow
|
// UI state for onboarding flow
|
||||||
private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
|
private var onboardingState by mutableStateOf(OnboardingState.CHECKING)
|
||||||
@@ -50,6 +64,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Initialize core mesh service first
|
||||||
|
meshService = BluetoothMeshService(this)
|
||||||
|
|
||||||
// Initialize permission management
|
// Initialize permission management
|
||||||
permissionManager = PermissionManager(this)
|
permissionManager = PermissionManager(this)
|
||||||
bluetoothStatusManager = BluetoothStatusManager(
|
bluetoothStatusManager = BluetoothStatusManager(
|
||||||
@@ -107,9 +124,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
onContinue = {
|
onContinue = {
|
||||||
onboardingState = OnboardingState.PERMISSION_REQUESTING
|
onboardingState = OnboardingState.PERMISSION_REQUESTING
|
||||||
onboardingCoordinator.requestPermissions()
|
onboardingCoordinator.requestPermissions()
|
||||||
},
|
|
||||||
onCancel = {
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -123,6 +137,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnboardingState.COMPLETE -> {
|
OnboardingState.COMPLETE -> {
|
||||||
|
// Set up back navigation handling for the chat screen
|
||||||
|
val backCallback = object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
// Let ChatViewModel handle navigation state
|
||||||
|
val handled = chatViewModel.handleBackPressed()
|
||||||
|
if (!handled) {
|
||||||
|
// If ChatViewModel doesn't handle it, disable this callback
|
||||||
|
// and let the system handle it (which will exit the app)
|
||||||
|
this.isEnabled = false
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
this.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the callback - this will be automatically removed when the activity is destroyed
|
||||||
|
onBackPressedDispatcher.addCallback(this, backCallback)
|
||||||
|
|
||||||
ChatScreen(viewModel = chatViewModel)
|
ChatScreen(viewModel = chatViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +321,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// This solves the issue where app needs restart to work on first install
|
// This solves the issue where app needs restart to work on first install
|
||||||
delay(1000) // Give the system time to process permission grants
|
delay(1000) // Give the system time to process permission grants
|
||||||
|
|
||||||
android.util.Log.d("MainActivity", "Permissions verified, starting mesh service")
|
android.util.Log.d("MainActivity", "Permissions verified, initializing chat system")
|
||||||
|
|
||||||
// Ensure all permissions are still granted (user might have revoked in settings)
|
// Ensure all permissions are still granted (user might have revoked in settings)
|
||||||
if (!permissionManager.areAllPermissionsGranted()) {
|
if (!permissionManager.areAllPermissionsGranted()) {
|
||||||
@@ -299,8 +331,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize chat view model - this will start the mesh service
|
// Set up mesh service delegate and start services
|
||||||
chatViewModel.meshService.startServices()
|
meshService.delegate = chatViewModel
|
||||||
|
meshService.startServices()
|
||||||
|
|
||||||
|
android.util.Log.d("MainActivity", "Mesh service started successfully")
|
||||||
|
|
||||||
// Handle any notification intent
|
// Handle any notification intent
|
||||||
handleNotificationIntent(intent)
|
handleNotificationIntent(intent)
|
||||||
@@ -330,6 +365,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
// Check Bluetooth status on resume and handle accordingly
|
// Check Bluetooth status on resume and handle accordingly
|
||||||
if (onboardingState == OnboardingState.COMPLETE) {
|
if (onboardingState == OnboardingState.COMPLETE) {
|
||||||
|
// Set app foreground state
|
||||||
|
meshService.connectionManager.setAppBackgroundState(false)
|
||||||
chatViewModel.setAppBackgroundState(false)
|
chatViewModel.setAppBackgroundState(false)
|
||||||
|
|
||||||
// Check if Bluetooth was disabled while app was backgrounded
|
// Check if Bluetooth was disabled while app was backgrounded
|
||||||
@@ -347,6 +384,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
// Only set background state if app is fully initialized
|
// Only set background state if app is fully initialized
|
||||||
if (onboardingState == OnboardingState.COMPLETE) {
|
if (onboardingState == OnboardingState.COMPLETE) {
|
||||||
|
// Set app background state
|
||||||
|
meshService.connectionManager.setAppBackgroundState(true)
|
||||||
chatViewModel.setAppBackgroundState(true)
|
chatViewModel.setAppBackgroundState(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,12 +415,32 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart mesh services (for debugging/troubleshooting)
|
||||||
|
*/
|
||||||
|
fun restartMeshServices() {
|
||||||
|
if (onboardingState == OnboardingState.COMPLETE) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
android.util.Log.d("MainActivity", "Restarting mesh services")
|
||||||
|
meshService.stopServices()
|
||||||
|
delay(1000)
|
||||||
|
meshService.startServices()
|
||||||
|
android.util.Log.d("MainActivity", "Mesh services restarted successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("MainActivity", "Error restarting mesh services: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
// Only stop mesh services if they were started
|
// Stop mesh services if app was fully initialized
|
||||||
if (onboardingState == OnboardingState.COMPLETE) {
|
if (onboardingState == OnboardingState.COMPLETE) {
|
||||||
try {
|
try {
|
||||||
chatViewModel.meshService.stopServices()
|
meshService.stopServices()
|
||||||
|
android.util.Log.d("MainActivity", "Mesh services stopped successfully")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
|
android.util.Log.w("MainActivity", "Error stopping mesh services in onDestroy: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,16 +132,16 @@ class BluetoothConnectionManager(
|
|||||||
try {
|
try {
|
||||||
isActive = true
|
isActive = true
|
||||||
|
|
||||||
|
// Setup GATT server first
|
||||||
|
setupGattServer()
|
||||||
|
|
||||||
// Start power manager and services
|
// Start power manager and services
|
||||||
connectionScope.launch {
|
connectionScope.launch {
|
||||||
powerManager.start()
|
powerManager.start()
|
||||||
|
delay(300) // Brief delay to ensure GATT server is ready
|
||||||
// Setup GATT server after power manager is ready
|
|
||||||
setupGattServer()
|
|
||||||
delay(500) // Ensure GATT server is ready
|
|
||||||
|
|
||||||
startAdvertising()
|
startAdvertising()
|
||||||
delay(200)
|
delay(100)
|
||||||
|
|
||||||
if (powerManager.shouldUseDutyCycle()) {
|
if (powerManager.shouldUseDutyCycle()) {
|
||||||
Log.i(TAG, "Using power-aware duty cycling")
|
Log.i(TAG, "Using power-aware duty cycling")
|
||||||
@@ -472,10 +472,15 @@ class BluetoothConnectionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (characteristic.uuid == CHARACTERISTIC_UUID) {
|
if (characteristic.uuid == CHARACTERISTIC_UUID) {
|
||||||
|
Log.d(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
|
||||||
val packet = BitchatPacket.fromBinaryData(value)
|
val packet = BitchatPacket.fromBinaryData(value)
|
||||||
if (packet != null) {
|
if (packet != null) {
|
||||||
val peerID = String(packet.senderID).replace("\u0000", "")
|
val peerID = String(packet.senderID).replace("\u0000", "")
|
||||||
|
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
|
||||||
delegate?.onPacketReceived(packet, peerID, device)
|
delegate?.onPacketReceived(packet, peerID, device)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Server: Failed to parse packet from ${device.address}, size: ${value.size} bytes")
|
||||||
|
Log.w(TAG, "Server: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseNeeded) {
|
if (responseNeeded) {
|
||||||
@@ -520,22 +525,22 @@ class BluetoothConnectionManager(
|
|||||||
// Proper cleanup sequencing to prevent race conditions
|
// Proper cleanup sequencing to prevent race conditions
|
||||||
gattServer?.let { server ->
|
gattServer?.let { server ->
|
||||||
Log.d(TAG, "Cleaning up existing GATT server")
|
Log.d(TAG, "Cleaning up existing GATT server")
|
||||||
connectionScope.launch {
|
try {
|
||||||
// Give time for pending callbacks to complete
|
|
||||||
delay(100)
|
|
||||||
server.close()
|
server.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Error closing existing GATT server: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new server after cleanup delay
|
// Small delay to ensure cleanup is complete
|
||||||
connectionScope.launch {
|
Thread.sleep(100)
|
||||||
delay(200) // Allow previous server to fully close
|
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
Log.d(TAG, "Service inactive, skipping GATT server creation")
|
Log.d(TAG, "Service inactive, skipping GATT server creation")
|
||||||
return@launch
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new server
|
||||||
gattServer = bluetoothManager.openGattServer(context, serverCallback)
|
gattServer = bluetoothManager.openGattServer(context, serverCallback)
|
||||||
|
|
||||||
// Create characteristic with notification support
|
// Create characteristic with notification support
|
||||||
@@ -562,7 +567,6 @@ class BluetoothConnectionManager(
|
|||||||
|
|
||||||
Log.i(TAG, "GATT server setup complete")
|
Log.i(TAG, "GATT server setup complete")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun startAdvertising() {
|
private fun startAdvertising() {
|
||||||
@@ -898,10 +902,15 @@ class BluetoothConnectionManager(
|
|||||||
|
|
||||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||||
val value = characteristic.value
|
val value = characteristic.value
|
||||||
|
Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
|
||||||
val packet = BitchatPacket.fromBinaryData(value)
|
val packet = BitchatPacket.fromBinaryData(value)
|
||||||
if (packet != null) {
|
if (packet != null) {
|
||||||
val peerID = String(packet.senderID).replace("\u0000", "")
|
val peerID = String(packet.senderID).replace("\u0000", "")
|
||||||
|
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
|
||||||
delegate?.onPacketReceived(packet, peerID, gatt.device)
|
delegate?.onPacketReceived(packet, peerID, gatt.device)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Client: Failed to parse packet from ${gatt.device.address}, size: ${value.size} bytes")
|
||||||
|
Log.w(TAG, "Client: Packet data: ${value.joinToString(" ") { "%02x".format(it) }}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
delay(10000) // 10 seconds
|
delay(10000) // 10 seconds
|
||||||
|
if (isActive) { // Double-check before logging
|
||||||
val debugInfo = getDebugStatus()
|
val debugInfo = getDebugStatus()
|
||||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===")
|
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
|
||||||
Log.d(TAG, debugInfo)
|
}
|
||||||
Log.d(TAG, "=== END DEBUG STATUS ===")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -287,7 +287,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
* Start the mesh service
|
* Start the mesh service
|
||||||
*/
|
*/
|
||||||
fun startServices() {
|
fun startServices() {
|
||||||
// Prevent double starts
|
// Prevent double starts (defensive programming)
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
Log.w(TAG, "Mesh service already active, ignoring duplicate start request")
|
Log.w(TAG, "Mesh service already active, ignoring duplicate start request")
|
||||||
return
|
return
|
||||||
@@ -302,10 +302,8 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Send initial announcements after services are ready
|
// Send initial announcements after services are ready
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
if (isActive) { // Check if still active
|
|
||||||
sendBroadcastAnnounce()
|
sendBroadcastAnnounce()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "Failed to start Bluetooth services")
|
Log.e(TAG, "Failed to start Bluetooth services")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,20 @@ import androidx.compose.ui.unit.sp
|
|||||||
@Composable
|
@Composable
|
||||||
fun PermissionExplanationScreen(
|
fun PermissionExplanationScreen(
|
||||||
permissionCategories: List<PermissionCategory>,
|
permissionCategories: List<PermissionCategory>,
|
||||||
onContinue: () -> Unit,
|
onContinue: () -> Unit
|
||||||
onCancel: () -> Unit
|
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
// Scrollable content
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
|
.padding(bottom = 88.dp) // Leave space for the fixed button
|
||||||
.verticalScroll(scrollState),
|
.verticalScroll(scrollState),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
@@ -125,16 +129,22 @@ fun PermissionExplanationScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
|
||||||
// Action buttons
|
// Fixed button at bottom
|
||||||
Column(
|
Surface(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = colorScheme.surface,
|
||||||
|
shadowElevation = 8.dp
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = onContinue,
|
onClick = onContinue,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = colorScheme.primary
|
containerColor = colorScheme.primary
|
||||||
)
|
)
|
||||||
@@ -148,25 +158,8 @@ fun PermissionExplanationScreen(
|
|||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onCancel,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
|
||||||
contentColor = colorScheme.onSurface.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Exit App",
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
fontFamily = FontFamily.Monospace
|
|
||||||
),
|
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -190,14 +183,14 @@ private fun PermissionCategoryCard(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = getPermissionEmoji(category.name),
|
text = getPermissionEmoji(category.type),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = getPermissionIconColor(category.name),
|
color = getPermissionIconColor(category.type),
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = category.name,
|
text = category.type.nameValue,
|
||||||
style = MaterialTheme.typography.titleSmall.copy(
|
style = MaterialTheme.typography.titleSmall.copy(
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = colorScheme.onSurface
|
color = colorScheme.onSurface
|
||||||
@@ -214,7 +207,7 @@ private fun PermissionCategoryCard(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (category.name == "Precise Location") {
|
if (category.type == PermissionType.PRECISE_LOCATION) {
|
||||||
// Extra emphasis for location permission
|
// Extra emphasis for location permission
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -239,20 +232,20 @@ private fun PermissionCategoryCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPermissionEmoji(categoryName: String): String {
|
private fun getPermissionEmoji(permissionType: PermissionType): String {
|
||||||
return when (categoryName) {
|
return when (permissionType) {
|
||||||
"Nearby Devices" -> "📱"
|
PermissionType.NEARBY_DEVICES -> "📱"
|
||||||
"Precise Location" -> "📍"
|
PermissionType.PRECISE_LOCATION -> "📍"
|
||||||
"Notifications" -> "🔔"
|
PermissionType.NOTIFICATIONS -> "🔔"
|
||||||
else -> "🔧"
|
PermissionType.OTHER -> "🔧"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPermissionIconColor(categoryName: String): Color {
|
private fun getPermissionIconColor(permissionType: PermissionType): Color {
|
||||||
return when (categoryName) {
|
return when (permissionType) {
|
||||||
"Nearby Devices" -> Color(0xFF2196F3) // Blue
|
PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue
|
||||||
"Precise Location" -> Color(0xFFFF9800) // Orange
|
PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange
|
||||||
"Notifications" -> Color(0xFF4CAF50) // Green
|
PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green
|
||||||
else -> Color(0xFF9C27B0) // Purple
|
PermissionType.OTHER -> Color(0xFF9C27B0) // Purple
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ class PermissionManager(private val context: Context) {
|
|||||||
|
|
||||||
categories.add(
|
categories.add(
|
||||||
PermissionCategory(
|
PermissionCategory(
|
||||||
name = "Nearby Devices",
|
type = PermissionType.NEARBY_DEVICES,
|
||||||
description = "Required to discover and connect to other bitchat users via Bluetooth",
|
description = "Required to discover bitchat users via Bluetooth",
|
||||||
permissions = bluetoothPermissions,
|
permissions = bluetoothPermissions,
|
||||||
isGranted = bluetoothPermissions.all { isPermissionGranted(it) },
|
isGranted = bluetoothPermissions.all { isPermissionGranted(it) },
|
||||||
systemDescription = "Allow bitchat to connect to nearby devices"
|
systemDescription = "Allow bitchat to connect to nearby devices"
|
||||||
@@ -131,11 +131,11 @@ class PermissionManager(private val context: Context) {
|
|||||||
|
|
||||||
categories.add(
|
categories.add(
|
||||||
PermissionCategory(
|
PermissionCategory(
|
||||||
name = "Precise Location",
|
type = PermissionType.PRECISE_LOCATION,
|
||||||
description = "Required by Android for Bluetooth scanning.",
|
description = "Required by Android to discover nearby bitchat users via Bluetooth",
|
||||||
permissions = locationPermissions,
|
permissions = locationPermissions,
|
||||||
isGranted = locationPermissions.all { isPermissionGranted(it) },
|
isGranted = locationPermissions.all { isPermissionGranted(it) },
|
||||||
systemDescription = "Allow bitchat to access this device's location"
|
systemDescription = "bitchat needs this to scan for nearby devices"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,8 +143,8 @@ class PermissionManager(private val context: Context) {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
categories.add(
|
categories.add(
|
||||||
PermissionCategory(
|
PermissionCategory(
|
||||||
name = "Notifications",
|
type = PermissionType.NOTIFICATIONS,
|
||||||
description = "Show notifications when you receive private messages while the app is in background",
|
description = "Receive notifications when you receive private messages",
|
||||||
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
permissions = listOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
isGranted = isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS),
|
isGranted = isPermissionGranted(Manifest.permission.POST_NOTIFICATIONS),
|
||||||
systemDescription = "Allow bitchat to send you notifications"
|
systemDescription = "Allow bitchat to send you notifications"
|
||||||
@@ -167,7 +167,7 @@ class PermissionManager(private val context: Context) {
|
|||||||
appendLine()
|
appendLine()
|
||||||
|
|
||||||
getCategorizedPermissions().forEach { category ->
|
getCategorizedPermissions().forEach { category ->
|
||||||
appendLine("${category.name}: ${if (category.isGranted) "✅ GRANTED" else "❌ MISSING"}")
|
appendLine("${category.type.nameValue}: ${if (category.isGranted) "✅ GRANTED" else "❌ MISSING"}")
|
||||||
category.permissions.forEach { permission ->
|
category.permissions.forEach { permission ->
|
||||||
val granted = isPermissionGranted(permission)
|
val granted = isPermissionGranted(permission)
|
||||||
appendLine(" - ${permission.substringAfterLast(".")}: ${if (granted) "✅" else "❌"}")
|
appendLine(" - ${permission.substringAfterLast(".")}: ${if (granted) "✅" else "❌"}")
|
||||||
@@ -197,9 +197,16 @@ class PermissionManager(private val context: Context) {
|
|||||||
* Data class representing a category of related permissions
|
* Data class representing a category of related permissions
|
||||||
*/
|
*/
|
||||||
data class PermissionCategory(
|
data class PermissionCategory(
|
||||||
val name: String,
|
val type: PermissionType,
|
||||||
val description: String,
|
val description: String,
|
||||||
val permissions: List<String>,
|
val permissions: List<String>,
|
||||||
val isGranted: Boolean,
|
val isGranted: Boolean,
|
||||||
val systemDescription: String
|
val systemDescription: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class PermissionType(val nameValue: String) {
|
||||||
|
NEARBY_DEVICES("Nearby Devices"),
|
||||||
|
PRECISE_LOCATION("Precise Location"),
|
||||||
|
NOTIFICATIONS("Notifications"),
|
||||||
|
OTHER("Other")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package com.bitchat.android.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.model.BitchatMessage
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message retention service for saving channel messages locally
|
||||||
|
* Matches iOS MessageRetentionService functionality
|
||||||
|
*/
|
||||||
|
class MessageRetentionService private constructor(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "MessageRetentionService"
|
||||||
|
private const val PREF_NAME = "message_retention"
|
||||||
|
private const val KEY_FAVORITE_CHANNELS = "favorite_channels"
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: MessageRetentionService? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): MessageRetentionService {
|
||||||
|
return INSTANCE ?: synchronized(this) {
|
||||||
|
INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
private val retentionDir = File(context.filesDir, "retained_messages")
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!retentionDir.exists()) {
|
||||||
|
retentionDir.mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Channel Bookmarking (Favorites)
|
||||||
|
|
||||||
|
fun getFavoriteChannels(): Set<String> {
|
||||||
|
return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleFavoriteChannel(channel: String): Boolean {
|
||||||
|
val currentFavorites = getFavoriteChannels().toMutableSet()
|
||||||
|
val wasAdded = if (currentFavorites.contains(channel)) {
|
||||||
|
currentFavorites.remove(channel)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
currentFavorites.add(channel)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply()
|
||||||
|
|
||||||
|
if (!wasAdded) {
|
||||||
|
// Channel removed from favorites - delete saved messages in background
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val channelFile = getChannelFile(channel)
|
||||||
|
if (channelFile.exists()) {
|
||||||
|
channelFile.delete()
|
||||||
|
Log.d(TAG, "Deleted saved messages for channel $channel")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to delete messages for channel $channel", e)
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}")
|
||||||
|
return wasAdded
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isChannelBookmarked(channel: String): Boolean {
|
||||||
|
return getFavoriteChannels().contains(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Storage
|
||||||
|
|
||||||
|
suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) {
|
||||||
|
if (!isChannelBookmarked(forChannel)) {
|
||||||
|
Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val channelFile = getChannelFile(forChannel)
|
||||||
|
val existingMessages = loadMessagesFromFile(channelFile).toMutableList()
|
||||||
|
|
||||||
|
// Check if message already exists (by ID)
|
||||||
|
if (existingMessages.any { it.id == message.id }) {
|
||||||
|
Log.d(TAG, "Message ${message.id} already saved for channel $forChannel")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new message
|
||||||
|
existingMessages.add(message)
|
||||||
|
|
||||||
|
// Sort by timestamp
|
||||||
|
existingMessages.sortBy { it.timestamp }
|
||||||
|
|
||||||
|
// Save back to file
|
||||||
|
saveMessagesToFile(channelFile, existingMessages)
|
||||||
|
|
||||||
|
Log.d(TAG, "Saved message ${message.id} for channel $forChannel")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to save message for channel $forChannel", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadMessagesForChannel(channel: String): List<BitchatMessage> = withContext(Dispatchers.IO) {
|
||||||
|
if (!isChannelBookmarked(channel)) {
|
||||||
|
Log.d(TAG, "Channel $channel not bookmarked, returning empty list")
|
||||||
|
return@withContext emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val channelFile = getChannelFile(channel)
|
||||||
|
val messages = loadMessagesFromFile(channelFile)
|
||||||
|
Log.d(TAG, "Loaded ${messages.size} messages for channel $channel")
|
||||||
|
return@withContext messages
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to load messages for channel $channel", e)
|
||||||
|
return@withContext emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val channelFile = getChannelFile(channel)
|
||||||
|
if (channelFile.exists()) {
|
||||||
|
channelFile.delete()
|
||||||
|
Log.d(TAG, "Deleted saved messages for channel $channel")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to delete messages for channel $channel", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
if (retentionDir.exists()) {
|
||||||
|
retentionDir.listFiles()?.forEach { file ->
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Deleted all stored messages")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to delete all stored messages", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - File Operations
|
||||||
|
|
||||||
|
private fun getChannelFile(channel: String): File {
|
||||||
|
// Sanitize channel name for filename
|
||||||
|
val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_")
|
||||||
|
return File(retentionDir, "channel_${sanitizedChannel}.dat")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadMessagesFromFile(file: File): List<BitchatMessage> {
|
||||||
|
if (!file.exists()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
FileInputStream(file).use { fis ->
|
||||||
|
ObjectInputStream(fis).use { ois ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
ois.readObject() as List<BitchatMessage>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMessagesToFile(file: File, messages: List<BitchatMessage>) {
|
||||||
|
FileOutputStream(file).use { fos ->
|
||||||
|
ObjectOutputStream(fos).use { oos ->
|
||||||
|
oos.writeObject(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Statistics
|
||||||
|
|
||||||
|
fun getBookmarkedChannelsCount(): Int {
|
||||||
|
return getFavoriteChannels().size
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) {
|
||||||
|
var totalCount = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
retentionDir.listFiles()?.forEach { file ->
|
||||||
|
if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) {
|
||||||
|
val messages = loadMessagesFromFile(file)
|
||||||
|
totalCount += messages.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to count stored messages", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
@@ -151,11 +152,17 @@ fun ChatHeaderContent(
|
|||||||
|
|
||||||
when {
|
when {
|
||||||
selectedPrivatePeer != null -> {
|
selectedPrivatePeer != null -> {
|
||||||
// Private chat header
|
// Private chat header - ensure state synchronization
|
||||||
|
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
||||||
|
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer)
|
||||||
|
val isFavorite = favoritePeers.contains(fingerprint)
|
||||||
|
|
||||||
|
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite")
|
||||||
|
|
||||||
PrivateChatHeader(
|
PrivateChatHeader(
|
||||||
peerID = selectedPrivatePeer,
|
peerID = selectedPrivatePeer,
|
||||||
peerNicknames = viewModel.meshService.getPeerNicknames(),
|
peerNicknames = viewModel.meshService.getPeerNicknames(),
|
||||||
isFavorite = viewModel.isFavorite(selectedPrivatePeer),
|
isFavorite = isFavorite,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
|
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
|
||||||
)
|
)
|
||||||
@@ -253,7 +260,10 @@ private fun PrivateChatHeader(
|
|||||||
|
|
||||||
// Favorite button - positioned on the right
|
// Favorite button - positioned on the right
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onToggleFavorite,
|
onClick = {
|
||||||
|
Log.d("ChatHeader", "Header toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
|
||||||
|
onToggleFavorite()
|
||||||
|
},
|
||||||
modifier = Modifier.align(Alignment.CenterEnd)
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -60,15 +60,15 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
||||||
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
||||||
val channelMessages by viewModel.channelMessages.observeAsState(emptyMap())
|
val channelMessages by viewModel.channelMessages.observeAsState(emptyMap())
|
||||||
var showSidebar by remember { mutableStateOf(false) }
|
val showSidebar by viewModel.showSidebar.observeAsState(false)
|
||||||
val showCommandSuggestions by viewModel.showCommandSuggestions.observeAsState(false)
|
val showCommandSuggestions by viewModel.showCommandSuggestions.observeAsState(false)
|
||||||
val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
|
val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
|
||||||
|
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
|
||||||
|
|
||||||
var messageText by remember { mutableStateOf("") }
|
var messageText by remember { mutableStateOf("") }
|
||||||
var showPasswordPrompt by remember { mutableStateOf(false) }
|
var showPasswordPrompt by remember { mutableStateOf(false) }
|
||||||
var showPasswordDialog by remember { mutableStateOf(false) }
|
var showPasswordDialog by remember { mutableStateOf(false) }
|
||||||
var passwordInput by remember { mutableStateOf("") }
|
var passwordInput by remember { mutableStateOf("") }
|
||||||
var showAppInfo by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// Show password dialog when needed
|
// Show password dialog when needed
|
||||||
LaunchedEffect(showPasswordPrompt) {
|
LaunchedEffect(showPasswordPrompt) {
|
||||||
@@ -142,8 +142,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
nickname = nickname,
|
nickname = nickname,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
onSidebarToggle = { showSidebar = true },
|
onSidebarToggle = { viewModel.showSidebar() },
|
||||||
onShowAppInfo = { showAppInfo = true },
|
onShowAppInfo = { viewModel.showAppInfo() },
|
||||||
onPanicClear = { viewModel.panicClearAllData() }
|
onPanicClear = { viewModel.panicClearAllData() }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
) {
|
) {
|
||||||
SidebarOverlay(
|
SidebarOverlay(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onDismiss = { showSidebar = false },
|
onDismiss = { viewModel.hideSidebar() },
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
passwordInput = ""
|
passwordInput = ""
|
||||||
},
|
},
|
||||||
showAppInfo = showAppInfo,
|
showAppInfo = showAppInfo,
|
||||||
onAppInfoDismiss = { showAppInfo = false }
|
onAppInfoDismiss = { viewModel.hideAppInfo() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@@ -84,6 +85,10 @@ class ChatState {
|
|||||||
|
|
||||||
val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>()
|
val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
// Navigation state
|
||||||
|
private val _showAppInfo = MutableLiveData<Boolean>(false)
|
||||||
|
val showAppInfo: LiveData<Boolean> = _showAppInfo
|
||||||
|
|
||||||
// Unread state computed properties
|
// Unread state computed properties
|
||||||
val hasUnreadChannels: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
val hasUnreadChannels: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
||||||
val hasUnreadPrivateMessages: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
val hasUnreadPrivateMessages: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
||||||
@@ -117,6 +122,7 @@ class ChatState {
|
|||||||
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
|
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
|
||||||
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
|
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
|
||||||
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
|
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
|
||||||
|
fun getShowAppInfoValue() = _showAppInfo.value ?: false
|
||||||
|
|
||||||
// Setters for state updates
|
// Setters for state updates
|
||||||
fun setMessages(messages: List<BitchatMessage>) {
|
fun setMessages(messages: List<BitchatMessage>) {
|
||||||
@@ -188,7 +194,21 @@ class ChatState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setFavoritePeers(favorites: Set<String>) {
|
fun setFavoritePeers(favorites: Set<String>) {
|
||||||
|
val currentValue = _favoritePeers.value ?: emptySet()
|
||||||
|
Log.d("ChatState", "setFavoritePeers called with ${favorites.size} favorites: $favorites")
|
||||||
|
Log.d("ChatState", "Current value: $currentValue")
|
||||||
|
Log.d("ChatState", "Values equal: ${currentValue == favorites}")
|
||||||
|
Log.d("ChatState", "Setting on thread: ${Thread.currentThread().name}")
|
||||||
|
|
||||||
|
// Always set the value - even if equal, this ensures observers are triggered
|
||||||
_favoritePeers.value = favorites
|
_favoritePeers.value = favorites
|
||||||
|
|
||||||
|
Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}")
|
||||||
|
Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setShowAppInfo(show: Boolean) {
|
||||||
|
_showAppInfo.value = show
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bitchat.android.ui
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -19,13 +20,13 @@ import kotlin.random.Random
|
|||||||
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
||||||
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility
|
* Delegates specific responsibilities to specialized managers while maintaining 100% iOS compatibility
|
||||||
*/
|
*/
|
||||||
class ChatViewModel(application: Application) : AndroidViewModel(application), BluetoothMeshDelegate {
|
class ChatViewModel(
|
||||||
|
application: Application,
|
||||||
|
val meshService: BluetoothMeshService
|
||||||
|
) : AndroidViewModel(application), BluetoothMeshDelegate {
|
||||||
|
|
||||||
private val context: Context = application.applicationContext
|
private val context: Context = application.applicationContext
|
||||||
|
|
||||||
// Core services
|
|
||||||
val meshService = BluetoothMeshService(context)
|
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
private val state = ChatState()
|
private val state = ChatState()
|
||||||
|
|
||||||
@@ -70,9 +71,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
||||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
||||||
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
||||||
|
val showAppInfo: LiveData<Boolean> = state.showAppInfo
|
||||||
|
|
||||||
init {
|
init {
|
||||||
meshService.delegate = this
|
// Note: Mesh service delegate is now set by MainActivity
|
||||||
loadAndInitialize()
|
loadAndInitialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +102,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
state.setFavoritePeers(dataManager.favoritePeers)
|
state.setFavoritePeers(dataManager.favoritePeers)
|
||||||
dataManager.loadBlockedUsers()
|
dataManager.loadBlockedUsers()
|
||||||
|
|
||||||
// Start mesh service
|
// Log all favorites at startup
|
||||||
meshService.startServices()
|
dataManager.logAllFavorites()
|
||||||
|
logCurrentFavoriteState()
|
||||||
|
|
||||||
|
// Note: Mesh service is now started by MainActivity
|
||||||
|
|
||||||
// Show welcome message if no peers after delay
|
// Show welcome message if no peers after delay
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -120,7 +125,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
meshService.stopServices()
|
// Note: Mesh service lifecycle is now managed by MainActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Nickname Management
|
// MARK: - Nickname Management
|
||||||
@@ -250,7 +255,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun toggleFavorite(peerID: String) {
|
fun toggleFavorite(peerID: String) {
|
||||||
|
Log.d("ChatViewModel", "toggleFavorite called for peerID: $peerID")
|
||||||
privateChatManager.toggleFavorite(peerID)
|
privateChatManager.toggleFavorite(peerID)
|
||||||
|
|
||||||
|
// Log current state after toggle
|
||||||
|
logCurrentFavoriteState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logCurrentFavoriteState() {
|
||||||
|
Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===")
|
||||||
|
Log.i("ChatViewModel", "LiveData favorite peers: ${favoritePeers.value}")
|
||||||
|
Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}")
|
||||||
|
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
|
||||||
|
Log.i("ChatViewModel", "==============================")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Debug and Troubleshooting
|
// MARK: - Debug and Troubleshooting
|
||||||
@@ -259,18 +276,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
return meshService.getDebugStatus()
|
return meshService.getDebugStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartMeshServices() {
|
// Note: Mesh service restart is now handled by MainActivity
|
||||||
viewModelScope.launch {
|
// This function is no longer needed
|
||||||
meshService.stopServices()
|
|
||||||
delay(1000)
|
|
||||||
meshService.startServices()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAppBackgroundState(inBackground: Boolean) {
|
fun setAppBackgroundState(inBackground: Boolean) {
|
||||||
// Forward to connection manager for power optimization
|
|
||||||
meshService.connectionManager.setAppBackgroundState(inBackground)
|
|
||||||
|
|
||||||
// Forward to notification manager for notification logic
|
// Forward to notification manager for notification logic
|
||||||
notificationManager.setAppBackgroundState(inBackground)
|
notificationManager.setAppBackgroundState(inBackground)
|
||||||
}
|
}
|
||||||
@@ -355,13 +364,62 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
state.setNickname(newNickname)
|
state.setNickname(newNickname)
|
||||||
dataManager.saveNickname(newNickname)
|
dataManager.saveNickname(newNickname)
|
||||||
|
|
||||||
// Disconnect from mesh
|
// Note: Mesh service restart is now handled by MainActivity
|
||||||
meshService.stopServices()
|
// This method now only clears data, not mesh service lifecycle
|
||||||
|
}
|
||||||
|
|
||||||
// Restart services with new identity
|
// MARK: - Navigation Management
|
||||||
viewModelScope.launch {
|
|
||||||
delay(500)
|
fun showAppInfo() {
|
||||||
meshService.startServices()
|
state.setShowAppInfo(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideAppInfo() {
|
||||||
|
state.setShowAppInfo(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSidebar() {
|
||||||
|
state.setShowSidebar(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideSidebar() {
|
||||||
|
state.setShowSidebar(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Android back navigation
|
||||||
|
* Returns true if the back press was handled, false if it should be passed to the system
|
||||||
|
*/
|
||||||
|
fun handleBackPressed(): Boolean {
|
||||||
|
return when {
|
||||||
|
// Close app info dialog
|
||||||
|
state.getShowAppInfoValue() -> {
|
||||||
|
hideAppInfo()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Close sidebar
|
||||||
|
state.getShowSidebarValue() -> {
|
||||||
|
hideSidebar()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Close password dialog
|
||||||
|
state.getShowPasswordPromptValue() -> {
|
||||||
|
state.setShowPasswordPrompt(false)
|
||||||
|
state.setPasswordPromptChannel(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Exit private chat
|
||||||
|
state.getSelectedPrivateChatPeerValue() != null -> {
|
||||||
|
endPrivateChat()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// Exit channel view
|
||||||
|
state.getCurrentChannelValue() != null -> {
|
||||||
|
switchToChannel(null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
// No special navigation state - let system handle (usually exits app)
|
||||||
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bitchat.android.ui
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Log
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -10,6 +11,10 @@ import kotlin.random.Random
|
|||||||
*/
|
*/
|
||||||
class DataManager(private val context: Context) {
|
class DataManager(private val context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DataManager"
|
||||||
|
}
|
||||||
|
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE)
|
private val prefs: SharedPreferences = context.getSharedPreferences("bitchat_prefs", Context.MODE_PRIVATE)
|
||||||
private val gson = Gson()
|
private val gson = Gson()
|
||||||
|
|
||||||
@@ -126,24 +131,41 @@ class DataManager(private val context: Context) {
|
|||||||
fun loadFavorites() {
|
fun loadFavorites() {
|
||||||
val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet()
|
val savedFavorites = prefs.getStringSet("favorites", emptySet()) ?: emptySet()
|
||||||
_favoritePeers.addAll(savedFavorites)
|
_favoritePeers.addAll(savedFavorites)
|
||||||
|
Log.d(TAG, "Loaded ${savedFavorites.size} favorite users from storage: $savedFavorites")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFavorites() {
|
fun saveFavorites() {
|
||||||
prefs.edit().putStringSet("favorites", _favoritePeers).apply()
|
prefs.edit().putStringSet("favorites", _favoritePeers).apply()
|
||||||
|
Log.d(TAG, "Saved ${_favoritePeers.size} favorite users to storage: $_favoritePeers")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addFavorite(fingerprint: String) {
|
fun addFavorite(fingerprint: String) {
|
||||||
_favoritePeers.add(fingerprint)
|
val wasAdded = _favoritePeers.add(fingerprint)
|
||||||
|
Log.d(TAG, "addFavorite: fingerprint=$fingerprint, wasAdded=$wasAdded")
|
||||||
saveFavorites()
|
saveFavorites()
|
||||||
|
logAllFavorites()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFavorite(fingerprint: String) {
|
fun removeFavorite(fingerprint: String) {
|
||||||
_favoritePeers.remove(fingerprint)
|
val wasRemoved = _favoritePeers.remove(fingerprint)
|
||||||
|
Log.d(TAG, "removeFavorite: fingerprint=$fingerprint, wasRemoved=$wasRemoved")
|
||||||
saveFavorites()
|
saveFavorites()
|
||||||
|
logAllFavorites()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFavorite(fingerprint: String): Boolean {
|
fun isFavorite(fingerprint: String): Boolean {
|
||||||
return _favoritePeers.contains(fingerprint)
|
val result = _favoritePeers.contains(fingerprint)
|
||||||
|
Log.d(TAG, "isFavorite check: fingerprint=$fingerprint, result=$result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun logAllFavorites() {
|
||||||
|
Log.i(TAG, "=== ALL FAVORITE USERS ===")
|
||||||
|
Log.i(TAG, "Total favorites: ${_favoritePeers.size}")
|
||||||
|
_favoritePeers.forEach { fingerprint ->
|
||||||
|
Log.i(TAG, "Favorite fingerprint: $fingerprint")
|
||||||
|
}
|
||||||
|
Log.i(TAG, "========================")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Blocked Users Management
|
// MARK: - Blocked Users Management
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.bitchat.android.ui
|
|||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles private chat functionality including peer management and blocking
|
* Handles private chat functionality including peer management and blocking
|
||||||
@@ -13,6 +14,10 @@ class PrivateChatManager(
|
|||||||
private val dataManager: DataManager
|
private val dataManager: DataManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "PrivateChatManager"
|
||||||
|
}
|
||||||
|
|
||||||
// Peer identification mapping
|
// Peer identification mapping
|
||||||
private val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>()
|
private val peerIDToPublicKeyFingerprint = mutableMapOf<String, String>()
|
||||||
|
|
||||||
@@ -99,17 +104,43 @@ class PrivateChatManager(
|
|||||||
fun toggleFavorite(peerID: String) {
|
fun toggleFavorite(peerID: String) {
|
||||||
val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return
|
val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return
|
||||||
|
|
||||||
if (dataManager.isFavorite(fingerprint)) {
|
Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint")
|
||||||
|
|
||||||
|
val wasFavorite = dataManager.isFavorite(fingerprint)
|
||||||
|
Log.d(TAG, "Current favorite status: $wasFavorite")
|
||||||
|
|
||||||
|
val currentFavorites = state.getFavoritePeersValue()
|
||||||
|
Log.d(TAG, "Current UI state favorites: $currentFavorites")
|
||||||
|
|
||||||
|
if (wasFavorite) {
|
||||||
dataManager.removeFavorite(fingerprint)
|
dataManager.removeFavorite(fingerprint)
|
||||||
|
Log.d(TAG, "Removed from favorites: $fingerprint")
|
||||||
} else {
|
} else {
|
||||||
dataManager.addFavorite(fingerprint)
|
dataManager.addFavorite(fingerprint)
|
||||||
|
Log.d(TAG, "Added to favorites: $fingerprint")
|
||||||
}
|
}
|
||||||
state.setFavoritePeers(dataManager.favoritePeers)
|
|
||||||
|
// Always update state to trigger UI refresh - create new set to ensure change detection
|
||||||
|
val newFavorites = dataManager.favoritePeers.toSet()
|
||||||
|
state.setFavoritePeers(newFavorites)
|
||||||
|
|
||||||
|
Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites")
|
||||||
|
Log.d(TAG, "All peer fingerprints: $peerIDToPublicKeyFingerprint")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFavorite(peerID: String): Boolean {
|
fun isFavorite(peerID: String): Boolean {
|
||||||
val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return false
|
val fingerprint = peerIDToPublicKeyFingerprint[peerID] ?: return false
|
||||||
return dataManager.isFavorite(fingerprint)
|
val isFav = dataManager.isFavorite(fingerprint)
|
||||||
|
Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav")
|
||||||
|
return isFav
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPeerFingerprint(peerID: String): String? {
|
||||||
|
return peerIDToPublicKeyFingerprint[peerID]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPeerFingerprints(): Map<String, String> {
|
||||||
|
return peerIDToPublicKeyFingerprint.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Block/Unblock Operations
|
// MARK: - Block/Unblock Operations
|
||||||
@@ -262,10 +293,6 @@ class PrivateChatManager(
|
|||||||
|
|
||||||
// MARK: - Public Getters
|
// MARK: - Public Getters
|
||||||
|
|
||||||
fun getPeerFingerprint(peerID: String): String? {
|
|
||||||
return peerIDToPublicKeyFingerprint[peerID]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAllPeerFingerprints(): Map<String, String> {
|
fun getAllPeerFingerprints(): Map<String, String> {
|
||||||
return peerIDToPublicKeyFingerprint.toMap()
|
return peerIDToPublicKeyFingerprint.toMap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
@@ -263,28 +264,40 @@ fun PeopleSection(
|
|||||||
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
||||||
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
||||||
|
|
||||||
|
// Pre-calculate all favorite states to ensure proper state synchronization
|
||||||
|
val peerFavoriteStates = remember(favoritePeers, connectedPeers) {
|
||||||
|
connectedPeers.associateWith { peerID ->
|
||||||
|
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID)
|
||||||
|
favoritePeers.contains(fingerprint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates")
|
||||||
|
|
||||||
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
|
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
|
||||||
val sortedPeers = connectedPeers.sortedWith(
|
val sortedPeers = connectedPeers.sortedWith(
|
||||||
compareBy<String> { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first
|
compareBy<String> { !hasUnreadPrivateMessages.contains(it) } // Unread DM senders first
|
||||||
.thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long)
|
.thenByDescending { privateChats[it]?.maxByOrNull { msg -> msg.timestamp }?.timestamp?.time ?: 0L } // Most recent DM (convert Date to Long)
|
||||||
.thenBy {
|
.thenBy { !(peerFavoriteStates[it] ?: false) } // Favorites first
|
||||||
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(it)
|
|
||||||
fingerprint == null || !favoritePeers.contains(fingerprint)
|
|
||||||
} // Favorites
|
|
||||||
.thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical
|
.thenBy { (if (it == nickname) "You" else (peerNicknames[it] ?: it)).lowercase() } // Alphabetical
|
||||||
)
|
)
|
||||||
|
|
||||||
sortedPeers.forEach { peerID ->
|
sortedPeers.forEach { peerID ->
|
||||||
|
val isFavorite = peerFavoriteStates[peerID] ?: false
|
||||||
|
|
||||||
PeerItem(
|
PeerItem(
|
||||||
peerID = peerID,
|
peerID = peerID,
|
||||||
displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID),
|
displayName = if (peerID == nickname) "You" else (peerNicknames[peerID] ?: peerID),
|
||||||
signalStrength = peerRSSI[peerID] ?: 0,
|
signalStrength = peerRSSI[peerID] ?: 0,
|
||||||
isSelected = peerID == selectedPrivatePeer,
|
isSelected = peerID == selectedPrivatePeer,
|
||||||
isFavorite = favoritePeers.contains(viewModel.privateChatManager.getPeerFingerprint(peerID)),
|
isFavorite = isFavorite,
|
||||||
hasUnreadDM = hasUnreadPrivateMessages.contains(peerID),
|
hasUnreadDM = hasUnreadPrivateMessages.contains(peerID),
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
onItemClick = { onPrivateChatStart(peerID) },
|
onItemClick = { onPrivateChatStart(peerID) },
|
||||||
onToggleFavorite = { viewModel.toggleFavorite(peerID) }
|
onToggleFavorite = {
|
||||||
|
Log.d("SidebarComponents", "Sidebar toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
|
||||||
|
viewModel.toggleFavorite(peerID)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:viewportWidth="135.4667"
|
||||||
|
android:viewportHeight="135.4667"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp">
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#2C2C2E"
|
android:pathData="M-0.2617076 -0.1145289H135.7284V135.5812H-0.2617076V-0.1145289Z"
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
android:fillColor="#000000" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,51 +1,15 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:viewportWidth="135.4667"
|
||||||
|
android:viewportHeight="135.4667"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp">
|
||||||
android:viewportWidth="108"
|
<group
|
||||||
android:viewportHeight="108">
|
android:scaleX="0.2645833"
|
||||||
<!-- Centered chat terminal window - safe zone is 66dp circle centered at 54,54 -->
|
android:scaleY="0.2645833"
|
||||||
<!-- Outer terminal border (28dp wide, 18dp tall, centered) -->
|
android:translateX="-1.0877"
|
||||||
|
android:translateY="47.11238">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#00FF00"
|
android:pathData="M187.45493 73.477844q9.17966 2.148433 14.38798 8.333313 5.20832 6.18488 5.20832 14.713504 0 12.499969 -9.11456 19.921819 -9.04945 7.42186 -21.80984 7.42186h-26.1067V32.006596h25.45566q13.28122 0 20.76818 5.85936 7.48696 5.794256 7.48696 16.406208 0 6.5104 -4.36197 11.588512 -4.29686 5.078112 -11.91403 7.356752zM160.69718 68.52994h12.49997q19.59631 0 19.59631 -13.99736 0 -6.380192 -4.8177 -9.7656 -4.8177 -3.450512 -14.12757 -3.450512h-13.15101zm0 46.02853h12.10935q10.61195 0 16.79683 -4.8177 6.24998 -4.81769 6.24998 -13.216109 0 -8.919248 -6.11977 -13.802048 -6.11978 -4.8828 -17.70829 -4.8828h-11.3281zm116.14554 9.30987h-41.73166v-9.30987h15.82027V41.316468h-15.82027v-9.309872h41.73166v9.309872h-15.23433v73.242002h15.23433zM370.20186 40.665428H341.88162V123.86834H331.26967V40.665428h-28.45045v-8.658832h67.38264z"
|
||||||
android:pathData="M40,45h28c2.2,0 4,1.8 4,4v10c0,2.2 -1.8,4 -4,4H40c-2.2,0 -4,-1.8 -4,-4V49C36,46.8 37.8,45 40,45Z"
|
android:fillColor="#FFFFFF" />
|
||||||
android:strokeWidth="0" />
|
</group>
|
||||||
|
|
||||||
<!-- Inner terminal background -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#1C1C1E"
|
|
||||||
android:pathData="M41,47h26c1.1,0 2,0.9 2,2v8c0,1.1 -0.9,2 -2,2H41c-1.1,0 -2,-0.9 -2,-2v-8C39,47.9 39.9,47 41,47Z"
|
|
||||||
android:strokeWidth="0" />
|
|
||||||
|
|
||||||
<!-- Terminal cursor block (centered left) -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#00FF00"
|
|
||||||
android:pathData="M43,51h3v4h-3z" />
|
|
||||||
|
|
||||||
<!-- Text indicators (representing chat messages) -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#00AA00"
|
|
||||||
android:pathData="M48,52h16v1.5h-16z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00AA00"
|
|
||||||
android:pathData="M48,54.5h12v1.5h-12z" />
|
|
||||||
|
|
||||||
<!-- Connection indicator dots (mesh network) - positioned in top right -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#00CC00"
|
|
||||||
android:pathData="M62,38m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00CC00"
|
|
||||||
android:pathData="M58.5,35m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00CC00"
|
|
||||||
android:pathData="M65.5,35m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
|
|
||||||
|
|
||||||
<!-- Additional connection lines to show mesh network -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#00AA00"
|
|
||||||
android:pathData="M59.5,36l3,2l3,-2"
|
|
||||||
android:strokeColor="#00AA00"
|
|
||||||
android:strokeWidth="0.5"
|
|
||||||
android:fillType="nonZero" />
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,60 +1,16 @@
|
|||||||
<vector
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:name="vector"
|
android:name="vector"
|
||||||
|
android:viewportWidth="135.4667"
|
||||||
|
android:viewportHeight="135.4667"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp">
|
||||||
android:viewportWidth="108"
|
<group
|
||||||
android:viewportHeight="108"
|
android:scaleX="0.2645833"
|
||||||
>
|
android:scaleY="0.2645833"
|
||||||
|
android:translateX="-1.0877"
|
||||||
|
android:translateY="47.11238">
|
||||||
<path
|
<path
|
||||||
android:name="path"
|
android:pathData="M187.45493 73.477844q9.17966 2.148433 14.38798 8.333313 5.20832 6.18488 5.20832 14.713504 0 12.499969 -9.11456 19.921819 -9.04945 7.42186 -21.80984 7.42186h-26.1067V32.006596h25.45566q13.28122 0 20.76818 5.85936 7.48696 5.794256 7.48696 16.406208 0 6.5104 -4.36197 11.588512 -4.29686 5.078112 -11.91403 7.356752zM160.69718 68.52994h12.49997q19.59631 0 19.59631 -13.99736 0 -6.380192 -4.8177 -9.7656 -4.8177 -3.450512 -14.12757 -3.450512h-13.15101zm0 46.02853h12.10935q10.61195 0 16.79683 -4.8177 6.24998 -4.81769 6.24998 -13.216109 0 -8.919248 -6.11977 -13.802048 -6.11978 -4.8828 -17.70829 -4.8828h-11.3281zm116.14554 9.30987h-41.73166v-9.30987h15.82027V41.316468h-15.82027v-9.309872h41.73166v9.309872h-15.23433v73.242002h15.23433zM370.20186 40.665428H341.88162V123.86834H331.26967V40.665428h-28.45045v-8.658832h67.38264z"
|
||||||
android:pathData="M 40 45 L 68 45 C 70.2 45 72 46.8 72 49 L 72 59 C 72 61.2 70.2 63 68 63 L 40 63 C 37.8 63 36 61.2 36 59 L 36 49 C 36 46.8 37.8 45 40 45 Z"
|
android:fillColor="#000000" />
|
||||||
android:fillColor="#00000000"
|
</group>
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_1"
|
|
||||||
android:pathData="M 41 47 L 67 47 C 68.1 47 69 47.9 69 49 L 69 57 C 69 58.1 68.1 59 67 59 L 41 59 C 39.9 59 39 58.1 39 57 L 39 49 C 39 47.9 39.9 47 41 47 Z"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="2"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_2"
|
|
||||||
android:pathData="M 43 51 L 46 51 L 46 55 L 43 55 Z"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_3"
|
|
||||||
android:pathData="M 48 52 L 64 52 L 64 53.5 L 48 53.5 Z"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_4"
|
|
||||||
android:pathData="M 48 54.5 L 60 54.5 L 60 56 L 48 56 Z"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_5"
|
|
||||||
android:pathData="M 62 38 M 60.5 38 C 60.5 37.602 60.658 37.221 60.939 36.939 C 61.221 36.658 61.602 36.5 62 36.5 C 62.398 36.5 62.779 36.658 63.061 36.939 C 63.342 37.221 63.5 37.602 63.5 38 C 63.5 38.398 63.342 38.779 63.061 39.061 C 62.779 39.342 62.398 39.5 62 39.5 C 61.602 39.5 61.221 39.342 60.939 39.061 C 60.658 38.779 60.5 38.398 60.5 38"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_6"
|
|
||||||
android:pathData="M 58.5 35 M 57.5 35 C 57.5 34.735 57.605 34.48 57.793 34.293 C 57.98 34.105 58.235 34 58.5 34 C 58.765 34 59.02 34.105 59.207 34.293 C 59.395 34.48 59.5 34.735 59.5 35 C 59.5 35.265 59.395 35.52 59.207 35.707 C 59.02 35.895 58.765 36 58.5 36 C 58.235 36 57.98 35.895 57.793 35.707 C 57.605 35.52 57.5 35.265 57.5 35"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_7"
|
|
||||||
android:pathData="M 65.5 35 M 64.5 35 C 64.5 34.735 64.605 34.48 64.793 34.293 C 64.98 34.105 65.235 34 65.5 34 C 65.765 34 66.02 34.105 66.207 34.293 C 66.395 34.48 66.5 34.735 66.5 35 C 66.5 35.265 66.395 35.52 66.207 35.707 C 66.02 35.895 65.765 36 65.5 36 C 65.235 36 64.98 35.895 64.793 35.707 C 64.605 35.52 64.5 35.265 64.5 35"
|
|
||||||
android:fillColor="#000000"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
android:name="path_8"
|
|
||||||
android:pathData="M 59 35.5 L 62.5 38 L 65 35.5"
|
|
||||||
android:fillColor="#55000000"
|
|
||||||
android:strokeColor="#000000"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 628 B After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 628 B |
|
Before Width: | Height: | Size: 765 B After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,62 +0,0 @@
|
|||||||
# Username Color Feature Demo
|
|
||||||
|
|
||||||
The bitchat Android app already has the username color feature fully implemented! Here's how it works:
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
1. **Your username remains green** - Uses `colorScheme.primary` (bright green in dark mode, dark green in light mode)
|
|
||||||
2. **Other users get unique colors** - Based on their peer ID using the `getUsernameColor()` function
|
|
||||||
3. **Colors are consistent** - Same user always gets the same color across sessions
|
|
||||||
4. **Terminal-friendly palette** - 16 colors that work on both black and white backgrounds
|
|
||||||
|
|
||||||
## Color Palette
|
|
||||||
|
|
||||||
The system uses these 16 terminal-friendly colors for other users:
|
|
||||||
|
|
||||||
- 🟢 Bright Green (#00FF00)
|
|
||||||
- 🔵 Cyan (#00FFFF)
|
|
||||||
- 🟡 Yellow (#FFFF00)
|
|
||||||
- 🔴 Magenta (#FF00FF)
|
|
||||||
- 🟦 Bright Blue (#0080FF)
|
|
||||||
- 🟠 Orange (#FF8000)
|
|
||||||
- 🔶 Lime Green (#80FF00)
|
|
||||||
- 🟣 Purple (#8000FF)
|
|
||||||
- 🩷 Pink (#FF0080)
|
|
||||||
- 💚 Spring Green (#00FF80)
|
|
||||||
- 🟦 Light Cyan (#80FFFF)
|
|
||||||
- 🩷 Light Red (#FF8080)
|
|
||||||
- 🟦 Light Blue (#8080FF)
|
|
||||||
- 🟡 Light Yellow (#FFFF80)
|
|
||||||
- 🩷 Light Magenta (#FF80FF)
|
|
||||||
- 🟢 Light Green (#80FF80)
|
|
||||||
|
|
||||||
## Example Chat Display
|
|
||||||
|
|
||||||
```
|
|
||||||
[14:23:45] <@you> hello everyone! ← Your message (green)
|
|
||||||
[14:23:47] <@alice> hey there! ← Alice (cyan)
|
|
||||||
[14:23:50] <@bob> how's it going? ← Bob (yellow)
|
|
||||||
[14:23:52] <@charlie> great to see you all ← Charlie (magenta)
|
|
||||||
[14:23:55] <@you> having a great time ← Your message (green)
|
|
||||||
[14:23:58] <@alice> same here @you! ← Alice (cyan again)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Implementation
|
|
||||||
|
|
||||||
The feature is implemented in `/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt`:
|
|
||||||
|
|
||||||
- Line 342-350: Color assignment logic in `formatMessageAsAnnotatedString()`
|
|
||||||
- Line 820-855: `getUsernameColor()` function that generates consistent colors
|
|
||||||
- Uses peer ID for consistency (falls back to nickname if no peer ID available)
|
|
||||||
- Integrates perfectly with the existing IRC-style chat format
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
The feature is already working in the app. When you chat with multiple users, you'll see:
|
|
||||||
- Your messages in green
|
|
||||||
- Each other user in their own unique color
|
|
||||||
- Same user always has the same color
|
|
||||||
- Colors remain consistent across app restarts
|
|
||||||
- Works in both light and dark themes
|
|
||||||
|
|
||||||
The feature is **complete and ready to use**! 🎉
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# BluetoothMeshService Refactoring Plan
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
- Single file: `BluetoothMeshService.kt` (~1000+ lines)
|
|
||||||
- Multiple responsibilities mixed together
|
|
||||||
- Hard to test and maintain
|
|
||||||
|
|
||||||
## Proposed Structure
|
|
||||||
|
|
||||||
### 1. Core Service (BluetoothMeshService.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Service lifecycle management
|
|
||||||
- Coordination between components
|
|
||||||
- Public API for sending messages
|
|
||||||
- Delegate management
|
|
||||||
|
|
||||||
### 2. Connection Management (BluetoothConnectionManager.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- BLE scanning and advertising
|
|
||||||
- GATT server/client setup and management
|
|
||||||
- Device connection tracking
|
|
||||||
- Peer discovery and RSSI tracking
|
|
||||||
|
|
||||||
### 3. Packet Processing (PacketProcessor.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Incoming packet handling
|
|
||||||
- Message type routing
|
|
||||||
- TTL and duplicate detection
|
|
||||||
- Timestamp validation
|
|
||||||
|
|
||||||
### 4. Message Handler (MessageHandler.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Processing specific message types (ANNOUNCE, MESSAGE, LEAVE, etc.)
|
|
||||||
- Message parsing and validation
|
|
||||||
- Relay logic
|
|
||||||
|
|
||||||
### 5. Fragment Manager (FragmentManager.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Message fragmentation for large messages
|
|
||||||
- Fragment reassembly
|
|
||||||
- Fragment cleanup and timeouts
|
|
||||||
|
|
||||||
### 6. Store-and-Forward Manager (StoreForwardManager.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Message caching for offline peers
|
|
||||||
- Delivering cached messages when peers come online
|
|
||||||
- Cache cleanup and management
|
|
||||||
|
|
||||||
### 7. Peer Manager (PeerManager.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Active peer tracking
|
|
||||||
- Peer nickname management
|
|
||||||
- Stale peer cleanup
|
|
||||||
- Peer list updates
|
|
||||||
|
|
||||||
### 8. Security Manager (SecurityManager.kt)
|
|
||||||
**Responsibilities:**
|
|
||||||
- Key exchange handling
|
|
||||||
- Message signing and verification
|
|
||||||
- Duplicate detection tracking
|
|
||||||
|
|
||||||
## Refactoring Strategy
|
|
||||||
1. Extract each component while maintaining exact functionality
|
|
||||||
2. Use dependency injection for component communication
|
|
||||||
3. Ensure all existing tests pass
|
|
||||||
4. Maintain the same public API
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
- Easier to test individual components
|
|
||||||
- Better separation of concerns
|
|
||||||
- More maintainable code
|
|
||||||
- Easier to add new features
|
|
||||||
- Better code reuse
|
|
||||||