mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 13:25:21 +00:00
Dm header icons reactive (#190)
* header reactive to noise session * favorite button reactive * icon change * nice
This commit is contained in:
@@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user