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)
}
/**
* 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
*/
@@ -863,6 +863,13 @@ class BluetoothMeshService(private val context: Context) {
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)
*/
@@ -145,6 +145,13 @@ class NoiseEncryptionService(private val context: Context) {
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
/**
@@ -157,6 +157,13 @@ class NoiseSessionManager(
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)
*/
@@ -29,6 +29,61 @@ import com.bitchat.android.core.ui.utils.singleOrTripleClickable
* 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
fun NicknameEditor(
value: String,
@@ -152,19 +207,26 @@ fun ChatHeaderContent(
when {
selectedPrivatePeer != null -> {
// Private chat header - ensure state synchronization
// Private chat header - Fully reactive state tracking
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer)
val isFavorite = favoritePeers.contains(fingerprint)
val hasEncryption = viewModel.meshService.shouldShowEncryptionIcon(selectedPrivatePeer)
val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
val peerSessionStates by viewModel.peerSessionStates.observeAsState(emptyMap())
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(
peerID = selectedPrivatePeer,
peerNicknames = viewModel.meshService.getPeerNicknames(),
isFavorite = isFavorite,
hasEncryption = hasEncryption,
sessionState = sessionState,
onBackClick = onBackClick,
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
)
@@ -197,7 +259,7 @@ private fun PrivateChatHeader(
peerID: String,
peerNicknames: Map<String, String>,
isFavorite: Boolean,
hasEncryption: Boolean,
sessionState: String?,
onBackClick: () -> Unit,
onToggleFavorite: () -> Unit
) {
@@ -240,23 +302,12 @@ private fun PrivateChatHeader(
verticalAlignment = Alignment.CenterVertically,
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
if (hasEncryption) {
Spacer(modifier = Modifier.width(2.dp))
Icon(
imageVector = Icons.Filled.Security,
contentDescription = "End-to-end encrypted",
modifier = Modifier.size(14.dp),
tint = Color(0xFF00C851) // Green to indicate verified encryption
// Reactive Noise session status icon
NoiseSessionIcon(
sessionState = sessionState,
modifier = Modifier.size(14.dp)
)
}
Spacer(modifier = Modifier.width(4.dp))
Text(
@@ -278,7 +329,7 @@ private fun PrivateChatHeader(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
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())
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
// Navigation state
@@ -122,6 +130,8 @@ class ChatState {
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
fun getPeerSessionStatesValue() = _peerSessionStates.value ?: emptyMap()
fun getPeerFingerprintsValue() = _peerFingerprints.value ?: emptyMap()
fun getShowAppInfoValue() = _showAppInfo.value ?: false
// Setters for state updates
@@ -207,6 +217,14 @@ class ChatState {
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) {
_showAppInfo.value = show
}
@@ -80,6 +80,8 @@ class ChatViewModel(
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
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
init {
@@ -115,6 +117,9 @@ class ChatViewModel(
dataManager.logAllFavorites()
logCurrentFavoriteState()
// Initialize session state monitoring
initializeSessionStateMonitoring()
// Note: Mesh service is now started by MainActivity
// Show welcome message if no peers after delay
@@ -279,6 +284,35 @@ class 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
fun getDebugStatus(): String {
@@ -270,16 +270,18 @@ fun PeopleSection(
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
)
} 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 privateChats by viewModel.privateChats.observeAsState(emptyMap())
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
// Pre-calculate all favorite states to ensure proper state synchronization
val peerFavoriteStates = remember(favoritePeers, connectedPeers) {
// Reactive favorite computation for all peers
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
connectedPeers.associateWith { peerID ->
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(peerID)
favoritePeers.contains(fingerprint)
// Reactive favorite computation - same as ChatHeader
val fingerprint = peerFingerprints[peerID]
fingerprint != null && favoritePeers.contains(fingerprint)
}
}
@@ -376,7 +378,7 @@ private fun PeerItem(
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
modifier = Modifier.size(16.dp),
tint = if (isFavorite) Color(0xFFFFD700) else colorScheme.primary
tint = if (isFavorite) Color(0xFFFFD700) else Color(0x87878700)
)
}
}