Dm header icons reactive (#190)

* header reactive to noise session

* favorite button reactive

* icon change

* nice
This commit is contained in:
callebtc
2025-07-25 19:50:40 +02:00
committed by GitHub
parent 271df2e720
commit c9aba00478
8 changed files with 166 additions and 33 deletions
@@ -169,6 +169,13 @@ class EncryptionService(private val context: Context) {
return noiseService.hasEstablishedSession(peerID) return noiseService.hasEstablishedSession(peerID)
} }
/**
* Get session state for a peer (for UI state display)
*/
fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState {
return noiseService.getSessionState(peerID)
}
/** /**
* Get encryption icon state for UI * Get encryption icon state for UI
*/ */
@@ -863,6 +863,13 @@ class BluetoothMeshService(private val context: Context) {
return encryptionService.hasEstablishedSession(peerID) return encryptionService.hasEstablishedSession(peerID)
} }
/**
* Get session state for a peer (for UI state display)
*/
fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState {
return encryptionService.getSessionState(peerID)
}
/** /**
* Initiate Noise handshake with a specific peer (public API) * Initiate Noise handshake with a specific peer (public API)
*/ */
@@ -145,6 +145,13 @@ class NoiseEncryptionService(private val context: Context) {
return sessionManager.hasEstablishedSession(peerID) return sessionManager.hasEstablishedSession(peerID)
} }
/**
* Get session state for a peer (for UI state display)
*/
fun getSessionState(peerID: String): NoiseSession.NoiseSessionState {
return sessionManager.getSessionState(peerID)
}
// MARK: - Encryption/Decryption // MARK: - Encryption/Decryption
/** /**
@@ -157,6 +157,13 @@ class NoiseSessionManager(
return hasSession return hasSession
} }
/**
* Get session state for a peer (for UI state display)
*/
fun getSessionState(peerID: String): NoiseSession.NoiseSessionState {
return getSession(peerID)?.getState() ?: NoiseSession.NoiseSessionState.Uninitialized
}
/** /**
* Get remote static public key for a peer (if session established) * Get remote static public key for a peer (if session established)
*/ */
@@ -29,6 +29,61 @@ import com.bitchat.android.core.ui.utils.singleOrTripleClickable
* Extracted from ChatScreen.kt for better organization * Extracted from ChatScreen.kt for better organization
*/ */
/**
* Reactive helper to compute favorite state from fingerprint mapping
* This eliminates the need for static isFavorite parameters and makes
* the UI reactive to fingerprint manager changes
*/
@Composable
fun isFavoriteReactive(
peerID: String,
peerFingerprints: Map<String, String>,
favoritePeers: Set<String>
): Boolean {
return remember(peerID, peerFingerprints, favoritePeers) {
val fingerprint = peerFingerprints[peerID]
fingerprint != null && favoritePeers.contains(fingerprint)
}
}
@Composable
fun NoiseSessionIcon(
sessionState: String?,
modifier: Modifier = Modifier
) {
val (icon, color, contentDescription) = when (sessionState) {
"uninitialized" -> Triple(
Icons.Outlined.NoEncryption,
Color(0x87878700), // Grey - ready to establish
"Ready for handshake"
)
"handshaking" -> Triple(
Icons.Outlined.Sync,
Color(0x87878700), // Grey - in progress
"Handshake in progress"
)
"established" -> Triple(
Icons.Filled.Lock,
Color(0xFFFF9500), // Orange - secure
"End-to-end encrypted"
)
else -> { // "failed" or any other state
Triple(
Icons.Outlined.Warning,
Color(0xFFFF4444), // Red - error
"Handshake failed"
)
}
}
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = modifier,
tint = color
)
}
@Composable @Composable
fun NicknameEditor( fun NicknameEditor(
value: String, value: String,
@@ -152,19 +207,26 @@ fun ChatHeaderContent(
when { when {
selectedPrivatePeer != null -> { selectedPrivatePeer != null -> {
// Private chat header - ensure state synchronization // Private chat header - Fully reactive state tracking
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer) val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
val isFavorite = favoritePeers.contains(fingerprint) val peerSessionStates by viewModel.peerSessionStates.observeAsState(emptyMap())
val hasEncryption = viewModel.meshService.shouldShowEncryptionIcon(selectedPrivatePeer)
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite, encrypted=$hasEncryption") // Reactive favorite computation - no more static lookups!
val isFavorite = isFavoriteReactive(
peerID = selectedPrivatePeer,
peerFingerprints = peerFingerprints,
favoritePeers = favoritePeers
)
val sessionState = peerSessionStates[selectedPrivatePeer]
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, isFav=$isFavorite, sessionState=$sessionState")
PrivateChatHeader( PrivateChatHeader(
peerID = selectedPrivatePeer, peerID = selectedPrivatePeer,
peerNicknames = viewModel.meshService.getPeerNicknames(), peerNicknames = viewModel.meshService.getPeerNicknames(),
isFavorite = isFavorite, isFavorite = isFavorite,
hasEncryption = hasEncryption, sessionState = sessionState,
onBackClick = onBackClick, onBackClick = onBackClick,
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) } onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
) )
@@ -197,7 +259,7 @@ private fun PrivateChatHeader(
peerID: String, peerID: String,
peerNicknames: Map<String, String>, peerNicknames: Map<String, String>,
isFavorite: Boolean, isFavorite: Boolean,
hasEncryption: Boolean, sessionState: String?,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onToggleFavorite: () -> Unit onToggleFavorite: () -> Unit
) { ) {
@@ -240,23 +302,12 @@ private fun PrivateChatHeader(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.Center) modifier = Modifier.align(Alignment.Center)
) { ) {
Icon(
imageVector = Icons.Filled.Lock,
contentDescription = "Private chat",
modifier = Modifier.size(16.dp),
tint = Color(0xFFFF9500) // Orange to match private message theme
)
// Show encryption status icon if session is established // Reactive Noise session status icon
if (hasEncryption) { NoiseSessionIcon(
Spacer(modifier = Modifier.width(2.dp)) sessionState = sessionState,
Icon( modifier = Modifier.size(14.dp)
imageVector = Icons.Filled.Security, )
contentDescription = "End-to-end encrypted",
modifier = Modifier.size(14.dp),
tint = Color(0xFF00C851) // Green to indicate verified encryption
)
}
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(
@@ -278,7 +329,7 @@ private fun PrivateChatHeader(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star, imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites", contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
modifier = Modifier.size(18.dp), // Slightly larger than sidebar icon modifier = Modifier.size(18.dp), // Slightly larger than sidebar icon
tint = if (isFavorite) Color(0xFFFFD700) else Color(0xFF4CAF50) // Yellow for filled, green for outlined tint = if (isFavorite) Color(0xFFFFD700) else Color(0x87878700) // Yellow or grey
) )
} }
} }
@@ -83,6 +83,14 @@ class ChatState {
private val _favoritePeers = MutableLiveData<Set<String>>(emptySet()) private val _favoritePeers = MutableLiveData<Set<String>>(emptySet())
val favoritePeers: LiveData<Set<String>> = _favoritePeers val favoritePeers: LiveData<Set<String>> = _favoritePeers
// Noise session states for peers (for reactive UI updates)
private val _peerSessionStates = MutableLiveData<Map<String, String>>(emptyMap())
val peerSessionStates: LiveData<Map<String, String>> = _peerSessionStates
// Peer fingerprint state for reactive favorites (for reactive UI updates)
private val _peerFingerprints = MutableLiveData<Map<String, String>>(emptyMap())
val peerFingerprints: LiveData<Map<String, String>> = _peerFingerprints
// peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager // peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager
// Navigation state // Navigation state
@@ -122,6 +130,8 @@ 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 getPeerSessionStatesValue() = _peerSessionStates.value ?: emptyMap()
fun getPeerFingerprintsValue() = _peerFingerprints.value ?: emptyMap()
fun getShowAppInfoValue() = _showAppInfo.value ?: false fun getShowAppInfoValue() = _showAppInfo.value ?: false
// Setters for state updates // Setters for state updates
@@ -207,6 +217,14 @@ class ChatState {
Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}") Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}")
} }
fun setPeerSessionStates(states: Map<String, String>) {
_peerSessionStates.value = states
}
fun setPeerFingerprints(fingerprints: Map<String, String>) {
_peerFingerprints.value = fingerprints
}
fun setShowAppInfo(show: Boolean) { fun setShowAppInfo(show: Boolean) {
_showAppInfo.value = show _showAppInfo.value = show
} }
@@ -80,6 +80,8 @@ class ChatViewModel(
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 peerSessionStates: LiveData<Map<String, String>> = state.peerSessionStates
val peerFingerprints: LiveData<Map<String, String>> = state.peerFingerprints
val showAppInfo: LiveData<Boolean> = state.showAppInfo val showAppInfo: LiveData<Boolean> = state.showAppInfo
init { init {
@@ -115,6 +117,9 @@ class ChatViewModel(
dataManager.logAllFavorites() dataManager.logAllFavorites()
logCurrentFavoriteState() logCurrentFavoriteState()
// Initialize session state monitoring
initializeSessionStateMonitoring()
// Note: Mesh service is now started by MainActivity // Note: Mesh service is now started by MainActivity
// Show welcome message if no peers after delay // Show welcome message if no peers after delay
@@ -279,6 +284,35 @@ class ChatViewModel(
Log.i("ChatViewModel", "==============================") Log.i("ChatViewModel", "==============================")
} }
/**
* Initialize session state monitoring for reactive UI updates
*/
private fun initializeSessionStateMonitoring() {
viewModelScope.launch {
while (true) {
delay(1000) // Check session states every second
updateReactiveStates()
}
}
}
/**
* Update reactive states for all connected peers (session states and fingerprints)
*/
private fun updateReactiveStates() {
val currentPeers = state.getConnectedPeersValue()
// Update session states
val sessionStates = currentPeers.associateWith { peerID ->
meshService.getSessionState(peerID).toString()
}
state.setPeerSessionStates(sessionStates)
// Update fingerprint mappings from centralized manager
val fingerprints = privateChatManager.getAllPeerFingerprints()
state.setPeerFingerprints(fingerprints)
}
// MARK: - Debug and Troubleshooting // MARK: - Debug and Troubleshooting
fun getDebugStatus(): String { fun getDebugStatus(): String {
@@ -270,22 +270,24 @@ fun PeopleSection(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
) )
} else { } else {
// Get unread private messages and private chat history for sorting // Observe reactive state for favorites and fingerprints
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 favoritePeers by viewModel.favoritePeers.observeAsState(emptySet()) val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
// Pre-calculate all favorite states to ensure proper state synchronization // Reactive favorite computation for all peers
val peerFavoriteStates = remember(favoritePeers, connectedPeers) { val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
connectedPeers.associateWith { peerID -> connectedPeers.associateWith { peerID ->
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID) // Reactive favorite computation - same as ChatHeader
favoritePeers.contains(fingerprint) val fingerprint = peerFingerprints[peerID]
fingerprint != null && favoritePeers.contains(fingerprint)
} }
} }
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates") 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)
@@ -376,7 +378,7 @@ private fun PeerItem(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star, imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites", contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
modifier = Modifier.size(16.dp), modifier = Modifier.size(16.dp),
tint = if (isFavorite) Color(0xFFFFD700) else colorScheme.primary tint = if (isFavorite) Color(0xFFFFD700) else Color(0x87878700)
) )
} }
} }