callebtc 5fd686a00a ui: celebrate being favorited in private chats
- header star wobbles and its outline turns orange when the peer
  favorites you; it only fills once you favorite them back
- mirror the 'favorited you' system notice into the private
  conversation (mesh path), matching the main chat
- expose reactive peerFavoritedUs state driven by
  FavoritesPersistenceService; keep system notices silent
  (no unread badge, read receipt or push)
2026-07-27 19:11:37 +02:00

358 lines
14 KiB
Kotlin

package com.bitchat.android.ui
import android.util.Log
import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/**
* Centralized state definitions and data classes for the chat system
*/
// Command suggestion data class
data class CommandSuggestion(
val command: String,
val aliases: List<String> = emptyList(),
val syntax: String? = null,
val description: String
)
/**
* Contains all the observable state for the chat system
*/
class ChatState(
scope: CoroutineScope
) {
// Core messages and peer state
private val _messages = MutableStateFlow<List<BitchatMessage>>(emptyList())
val messages: StateFlow<List<BitchatMessage>> = _messages.asStateFlow()
private val _connectedPeers = MutableStateFlow<List<String>>(emptyList())
val connectedPeers: StateFlow<List<String>> = _connectedPeers.asStateFlow()
private val _nickname = MutableStateFlow<String>("")
val nickname: StateFlow<String> = _nickname.asStateFlow()
private val _isConnected = MutableStateFlow<Boolean>(false)
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
// Private chats
private val _privateChats = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
val privateChats: StateFlow<Map<String, List<BitchatMessage>>> = _privateChats.asStateFlow()
private val _selectedPrivateChatPeer = MutableStateFlow<String?>(null)
val selectedPrivateChatPeer: StateFlow<String?> = _selectedPrivateChatPeer.asStateFlow()
private val _unreadPrivateMessages = MutableStateFlow<Set<String>>(emptySet())
val unreadPrivateMessages: StateFlow<Set<String>> = _unreadPrivateMessages.asStateFlow()
// Channels
private val _joinedChannels = MutableStateFlow<Set<String>>(emptySet())
val joinedChannels: StateFlow<Set<String>> = _joinedChannels.asStateFlow()
private val _currentChannel = MutableStateFlow<String?>(null)
val currentChannel: StateFlow<String?> = _currentChannel.asStateFlow()
private val _channelMessages = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = _channelMessages.asStateFlow()
private val _unreadChannelMessages = MutableStateFlow<Map<String, Int>>(emptyMap())
val unreadChannelMessages: StateFlow<Map<String, Int>> = _unreadChannelMessages.asStateFlow()
private val _passwordProtectedChannels = MutableStateFlow<Set<String>>(emptySet())
val passwordProtectedChannels: StateFlow<Set<String>> = _passwordProtectedChannels.asStateFlow()
private val _showPasswordPrompt = MutableStateFlow<Boolean>(false)
val showPasswordPrompt: StateFlow<Boolean> = _showPasswordPrompt.asStateFlow()
private val _passwordPromptChannel = MutableStateFlow<String?>(null)
val passwordPromptChannel: StateFlow<String?> = _passwordPromptChannel.asStateFlow()
// Command autocomplete
private val _showCommandSuggestions = MutableStateFlow(false)
val showCommandSuggestions: StateFlow<Boolean> = _showCommandSuggestions.asStateFlow()
private val _commandSuggestions = MutableStateFlow<List<CommandSuggestion>>(emptyList())
val commandSuggestions: StateFlow<List<CommandSuggestion>> = _commandSuggestions.asStateFlow()
// Mention autocomplete
private val _showMentionSuggestions = MutableStateFlow(false)
val showMentionSuggestions: StateFlow<Boolean> = _showMentionSuggestions.asStateFlow()
private val _mentionSuggestions = MutableStateFlow<List<String>>(emptyList())
val mentionSuggestions: StateFlow<List<String>> = _mentionSuggestions.asStateFlow()
// Favorites
private val _favoritePeers = MutableStateFlow<Set<String>>(emptySet())
val favoritePeers: StateFlow<Set<String>> = _favoritePeers.asStateFlow()
// Fingerprints of peers who favorited us (drives "favorited you" UI celebrations)
private val _peerFavoritedUs = MutableStateFlow<Set<String>>(emptySet())
val peerFavoritedUs: StateFlow<Set<String>> = _peerFavoritedUs.asStateFlow()
// Noise session states for peers (for reactive UI updates)
private val _peerSessionStates = MutableStateFlow<Map<String, String>>(emptyMap())
val peerSessionStates: StateFlow<Map<String, String>> = _peerSessionStates.asStateFlow()
// Peer fingerprint state for reactive favorites (for reactive UI updates)
private val _peerFingerprints = MutableStateFlow<Map<String, String>>(emptyMap())
val peerFingerprints: StateFlow<Map<String, String>> = _peerFingerprints.asStateFlow()
private val _peerNicknames = MutableStateFlow<Map<String, String>>(emptyMap())
val peerNicknames: StateFlow<Map<String, String>> = _peerNicknames.asStateFlow()
private val _peerRSSI = MutableStateFlow<Map<String, Int>>(emptyMap())
val peerRSSI: StateFlow<Map<String, Int>> = _peerRSSI.asStateFlow()
// Direct connection status per peer (for live UI updates)
private val _peerDirect = MutableStateFlow<Map<String, Boolean>>(emptyMap())
val peerDirect: StateFlow<Map<String, Boolean>> = _peerDirect.asStateFlow()
// peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager
// Navigation state
private val _showAppInfo = MutableStateFlow<Boolean>(false)
val showAppInfo: StateFlow<Boolean> = _showAppInfo.asStateFlow()
private val _showMeshPeerList = MutableStateFlow(false)
val showMeshPeerList: StateFlow<Boolean> = _showMeshPeerList.asStateFlow()
private val _privateChatSheetPeer = MutableStateFlow<String?>(null)
val privateChatSheetPeer: StateFlow<String?> = _privateChatSheetPeer.asStateFlow()
private val _showVerificationSheet = MutableStateFlow(false)
val showVerificationSheet: StateFlow<Boolean> = _showVerificationSheet.asStateFlow()
private val _showSecurityVerificationSheet = MutableStateFlow(false)
val showSecurityVerificationSheet: StateFlow<Boolean> = _showSecurityVerificationSheet.asStateFlow()
// Location channels state (for Nostr geohash features)
private val _selectedLocationChannel = MutableStateFlow<com.bitchat.android.geohash.ChannelID?>(com.bitchat.android.geohash.ChannelID.Mesh)
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = _selectedLocationChannel.asStateFlow()
private val _isTeleported = MutableStateFlow<Boolean>(false)
val isTeleported: StateFlow<Boolean> = _isTeleported.asStateFlow()
// Geohash people state (iOS-compatible)
private val _geohashPeople = MutableStateFlow<List<GeoPerson>>(emptyList())
val geohashPeople: StateFlow<List<GeoPerson>> = _geohashPeople.asStateFlow()
private val _teleportedGeo = MutableStateFlow<Set<String>>(emptySet())
val teleportedGeo: StateFlow<Set<String>> = _teleportedGeo.asStateFlow()
// Geohash participant counts reactive state (for real-time location channel counts)
private val _geohashParticipantCounts = MutableStateFlow<Map<String, Int>>(emptyMap())
val geohashParticipantCounts: StateFlow<Map<String, Int>> = _geohashParticipantCounts.asStateFlow()
val hasUnreadChannels: StateFlow<Boolean> = _unreadChannelMessages
.map { unreadMap -> unreadMap.values.any { it > 0 } }
.stateIn(
scope = scope,
started = WhileSubscribed(5_000),
initialValue = false
)
val hasUnreadPrivateMessages: StateFlow<Boolean> = _unreadPrivateMessages
.map { unreadSet -> unreadSet.isNotEmpty() }
.stateIn(
scope = scope,
started = WhileSubscribed(5_000),
initialValue = false
)
// Getters for internal state access
fun getMessagesValue() = _messages.value
fun getConnectedPeersValue() = _connectedPeers.value
fun getNicknameValue() = _nickname.value
fun getPrivateChatsValue() = _privateChats.value
fun getSelectedPrivateChatPeerValue() = _selectedPrivateChatPeer.value
fun getUnreadPrivateMessagesValue() = _unreadPrivateMessages.value
fun getJoinedChannelsValue() = _joinedChannels.value
fun getPeerFavoritedUsValue() = _peerFavoritedUs.value
fun getCurrentChannelValue() = _currentChannel.value
fun getChannelMessagesValue() = _channelMessages.value
fun getUnreadChannelMessagesValue() = _unreadChannelMessages.value
fun getPasswordProtectedChannelsValue() = _passwordProtectedChannels.value
fun getShowPasswordPromptValue() = _showPasswordPrompt.value
fun getPasswordPromptChannelValue() = _passwordPromptChannel.value
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value
fun getCommandSuggestionsValue() = _commandSuggestions.value
fun getShowMentionSuggestionsValue() = _showMentionSuggestions.value
fun getMentionSuggestionsValue() = _mentionSuggestions.value
fun getFavoritePeersValue() = _favoritePeers.value
fun getPeerSessionStatesValue() = _peerSessionStates.value
fun getPeerFingerprintsValue() = _peerFingerprints.value
fun getShowAppInfoValue() = _showAppInfo.value
fun getGeohashPeopleValue() = _geohashPeople.value
fun getShowMeshPeerListValue() = _showMeshPeerList.value
fun getPrivateChatSheetPeerValue() = _privateChatSheetPeer.value
fun getTeleportedGeoValue() = _teleportedGeo.value
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value
// Setters for state updates
fun setMessages(messages: List<BitchatMessage>) {
_messages.value = messages
}
fun setConnectedPeers(peers: List<String>) {
_connectedPeers.value = peers
}
fun postTeleportedGeo(teleported: Set<String>) {
_teleportedGeo.value = teleported
}
fun setNickname(nickname: String) {
_nickname.value = nickname
}
fun setIsConnected(connected: Boolean) {
_isConnected.value = connected
}
fun setPrivateChats(chats: Map<String, List<BitchatMessage>>) {
_privateChats.value = chats
}
fun setSelectedPrivateChatPeer(peerID: String?) {
_selectedPrivateChatPeer.value = peerID
}
fun setUnreadPrivateMessages(unread: Set<String>) {
_unreadPrivateMessages.value = unread
}
fun setJoinedChannels(channels: Set<String>) {
_joinedChannels.value = channels
}
fun setCurrentChannel(channel: String?) {
_currentChannel.value = channel
}
fun setChannelMessages(messages: Map<String, List<BitchatMessage>>) {
_channelMessages.value = messages
}
fun setUnreadChannelMessages(unread: Map<String, Int>) {
_unreadChannelMessages.value = unread
}
fun setPasswordProtectedChannels(channels: Set<String>) {
_passwordProtectedChannels.value = channels
}
fun setShowPasswordPrompt(show: Boolean) {
_showPasswordPrompt.value = show
}
fun setPasswordPromptChannel(channel: String?) {
_passwordPromptChannel.value = channel
}
fun setShowCommandSuggestions(show: Boolean) {
_showCommandSuggestions.value = show
}
fun setCommandSuggestions(suggestions: List<CommandSuggestion>) {
_commandSuggestions.value = suggestions
}
fun setShowMentionSuggestions(show: Boolean) {
_showMentionSuggestions.value = show
}
fun setMentionSuggestions(suggestions: List<String>) {
_mentionSuggestions.value = suggestions
}
fun setFavoritePeers(favorites: Set<String>) {
val currentValue = _favoritePeers.value
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
Log.d("ChatState", "StateFlow value after set: ${_favoritePeers.value}")
}
fun setPeerFavoritedUs(fingerprints: Set<String>) {
_peerFavoritedUs.value = fingerprints
}
fun setPeerSessionStates(states: Map<String, String>) {
_peerSessionStates.value = states
}
fun setPeerFingerprints(fingerprints: Map<String, String>) {
_peerFingerprints.value = fingerprints
}
fun setPeerNicknames(nicknames: Map<String, String>) {
_peerNicknames.value = nicknames
}
fun setPeerRSSI(rssi: Map<String, Int>) {
_peerRSSI.value = rssi
}
fun setPeerDirect(direct: Map<String, Boolean>) {
_peerDirect.value = direct
}
fun setShowAppInfo(show: Boolean) {
_showAppInfo.value = show
}
fun setShowVerificationSheet(show: Boolean) {
_showVerificationSheet.value = show
}
fun setShowSecurityVerificationSheet(show: Boolean) {
_showSecurityVerificationSheet.value = show
}
fun setSelectedLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
_selectedLocationChannel.value = channel
}
fun setIsTeleported(teleported: Boolean) {
_isTeleported.value = teleported
}
fun setGeohashPeople(people: List<GeoPerson>) {
_geohashPeople.value = people
}
fun setTeleportedGeo(teleported: Set<String>) {
_teleportedGeo.value = teleported
}
fun setGeohashParticipantCounts(counts: Map<String, Int>) {
_geohashParticipantCounts.value = counts
}
fun setShowMeshPeerList(show: Boolean) {
_showMeshPeerList.value = show
}
fun setPrivateChatSheetPeer(peerID: String?) {
_privateChatSheetPeer.value = peerID
}
}