mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 17:45:20 +00:00
Merge pull request #82 from permissionlesstech/fix-startup-2
Fix startup 2
This commit is contained in:
@@ -17,6 +17,8 @@ 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 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 +31,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 +62,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(
|
||||||
@@ -289,7 +304,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 +314,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 +348,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 +367,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 +398,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}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,16 +129,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")
|
||||||
@@ -410,10 +410,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) {
|
||||||
@@ -458,48 +463,47 @@ 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
|
||||||
}
|
|
||||||
|
|
||||||
gattServer = bluetoothManager.openGattServer(context, serverCallback)
|
|
||||||
|
|
||||||
// Create characteristic with notification support
|
|
||||||
characteristic = BluetoothGattCharacteristic(
|
|
||||||
CHARACTERISTIC_UUID,
|
|
||||||
BluetoothGattCharacteristic.PROPERTY_READ or
|
|
||||||
BluetoothGattCharacteristic.PROPERTY_WRITE or
|
|
||||||
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
|
|
||||||
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
|
|
||||||
BluetoothGattCharacteristic.PERMISSION_READ or
|
|
||||||
BluetoothGattCharacteristic.PERMISSION_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
val descriptor = BluetoothGattDescriptor(
|
|
||||||
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
|
|
||||||
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
|
|
||||||
)
|
|
||||||
characteristic?.addDescriptor(descriptor)
|
|
||||||
|
|
||||||
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
|
||||||
service.addCharacteristic(characteristic)
|
|
||||||
|
|
||||||
gattServer?.addService(service)
|
|
||||||
|
|
||||||
Log.i(TAG, "GATT server setup complete")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new server
|
||||||
|
gattServer = bluetoothManager.openGattServer(context, serverCallback)
|
||||||
|
|
||||||
|
// Create characteristic with notification support
|
||||||
|
characteristic = BluetoothGattCharacteristic(
|
||||||
|
CHARACTERISTIC_UUID,
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_READ or
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_WRITE or
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
|
||||||
|
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
|
||||||
|
BluetoothGattCharacteristic.PERMISSION_READ or
|
||||||
|
BluetoothGattCharacteristic.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
|
||||||
|
val descriptor = BluetoothGattDescriptor(
|
||||||
|
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
|
||||||
|
BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
|
||||||
|
)
|
||||||
|
characteristic?.addDescriptor(descriptor)
|
||||||
|
|
||||||
|
val service = BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
|
||||||
|
service.addCharacteristic(characteristic)
|
||||||
|
|
||||||
|
gattServer?.addService(service)
|
||||||
|
|
||||||
|
Log.i(TAG, "GATT server setup complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@@ -836,10 +840,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) }}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
delay(10000) // 10 seconds
|
delay(10000) // 10 seconds
|
||||||
val debugInfo = getDebugStatus()
|
if (isActive) { // Double-check before logging
|
||||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===")
|
val debugInfo = getDebugStatus()
|
||||||
Log.d(TAG, debugInfo)
|
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
|
||||||
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}")
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,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
|
||||||
@@ -297,9 +297,7 @@ 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")
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -188,7 +189,17 @@ 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()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application), B
|
|||||||
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
||||||
|
|
||||||
init {
|
init {
|
||||||
meshService.delegate = this
|
// Note: Mesh service delegate is now set by MainActivity
|
||||||
loadAndInitialize()
|
loadAndInitialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,8 +101,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 +124,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 +254,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 +275,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 +363,7 @@ 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
|
|
||||||
viewModelScope.launch {
|
|
||||||
delay(500)
|
|
||||||
meshService.startServices()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user