mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 06:25:21 +00:00
Migrate from LiveData to Kotlin Flow (#518)
* Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025
* Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025
* refactor: new close button like ios(but not liquid glass)
* Automated update of relay data - Sun Oct 5 06:20:09 UTC 2025
* Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025
* Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025
* Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025
* Automated update of relay data - Sun Nov 2 06:22:16 UTC 2025
* Automated update of relay data - Sun Nov 9 06:21:43 UTC 2025
* Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025
* Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025
* Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025
* Chore: Remove unused `lifecycle-livedata-ktx` dependency
This commit removes the `androidx.lifecycle:lifecycle-livedata-ktx` library from the project's dependencies.
The `[libraries]` and `[bundles]` sections in `gradle/libs.versions.toml` have been updated to reflect this removal, as the dependency is no longer in use.
* Refactor: Remove unused `runtime-livedata` dependency
* Refactor: Migrate `LocationChannelManager` and `GeohashBookmarksStore` to StateFlow
This commit refactors `LocationChannelManager` and `GeohashBookmarksStore` to use `StateFlow` instead of `LiveData` for managing and exposing their state. This change aligns with modern Android development practices and improves testability.
**Key Changes:**
- **`LocationChannelManager`**:
- All `MutableLiveData` properties (`permissionState`, `availableChannels`, `selectedChannel`, etc.) have been replaced with `MutableStateFlow`.
- Consumers now access these properties as `StateFlow`.
- State updates have been changed from `postValue()` to direct `.value` assignments, simplifying thread management within the manager which already uses a dedicated coroutine scope.
- **`GeohashBookmarksStore`**:
- `bookmarks` and `bookmarkNames` are now exposed as `StateFlow` instead of `LiveData`.
- State updates similarly use `.value` assignment.
- **Nullability**:
- The non-nullable nature of `StateFlow`'s value reduces the need for null-checks in both the manager classes and their consumers, leading to safer code.
* Refactor: Migrate from LiveData to StateFlow for Nostr components
This commit replaces `LiveData` with `StateFlow` across core Nostr-related classes to align with modern Android architecture and improve state management. This change affects `NostrClient`, `NostrRelayManager`, `LocationNotesManager`, and `GeohashRepository`.
**Key Changes:**
- **`NostrClient`**:
- `isInitialized` and `currentNpub` are now `StateFlow` instead of `LiveData`.
- `relayConnectionStatus` and `relayInfo` now return `StateFlow` from `NostrRelayManager`.
- **`NostrRelayManager`**:
- Public properties `relays` and `isConnected` are migrated from `MutableLiveData` to `MutableStateFlow`.
- Updates are now pushed using `.value` instead of `.postValue()`.
- **`LocationNotesManager`**:
- All public `LiveData` properties (`notes`, `geohash`, `initialLoadComplete`, `state`, `errorMessage`) are converted to `StateFlow`.
- The class documentation is updated to reflect the use of `StateFlow`.
- **`GeohashRepository`**:
- Methods `updateGeohashPeople` and `updateReactiveParticipantCounts` now call `set...` methods on the `state` object instead of `post...`, reflecting the removal of `LiveData` from the underlying state management.
* Refactor: Migrate ChatState from LiveData to StateFlow
This commit refactors the `ChatState`, `ChatViewModel`, and `GeohashViewModel` to use `StateFlow` instead of `LiveData` for managing and exposing UI state. This migration improves state management by leveraging modern coroutine-based flows.
**Key Changes:**
- **`ChatState.kt`**:
- Replaced all `MutableLiveData` instances with `MutableStateFlow`.
- Exposed state properties as `StateFlow` instead of `LiveData`.
- Removed `MediatorLiveData` for computed properties (`hasUnreadChannels`, `hasUnreadPrivateMessages`) and replaced them with `Flow.combine` to create derivative `StateFlows`.
- Simplified non-nullable `getters` to directly return the `.value` of the `StateFlows`.
- Removed `postValue` helpers that are no longer necessary.
- **`ChatViewModel.kt`**:
- Updated all state properties to be `StateFlow`, reflecting the changes in `ChatState`.
- **`GeohashViewModel.kt`**:
- Changed state properties (`geohashPeople`, `geohashParticipantCounts`, etc.) from `LiveData` to `StateFlow`.
- Replaced `observeForever` on `LiveData` from `LocationChannelManager` with `viewModelScope.launch` blocks that `.collect()` from the underlying flows.
* Refactor: Migrate UI from LiveData to StateFlow
This commit replaces `LiveData.observeAsState()` with `StateFlow.collectAsState()` across various UI components. This change aligns the codebase with modern Android development practices, using Kotlin Flows for reactive UI state management.
No functional changes are intended. The primary goal is to remove the dependency on `androidx.lifecycle.livedata` from the composable functions.
**Affected Components:**
- `ChatScreen`
- `SidebarComponents`
- `ChatHeader`
- `LocationChannelsSheet`
- `LocationNotesSheet`
- `LocationNotesButton`
- `GeohashPeopleList`
- `LocationNotesSheetPresenter`
* Refactor: Use `collectAsStateWithLifecycle` for UI state collection
* Refactor: move CloseButton to core/ui/component
* Refactor: remove AI generated comments
* Refactor: fix combine to map and use WhileSubscribed
* Refactor: Pass CoroutineScope to ChatState
This commit refactors the `ChatState` class to accept a `CoroutineScope` in its constructor instead of creating its own.
**Key Changes:**
- **`ChatState.kt`**: The constructor now requires a `CoroutineScope`. This scope is used for the `stateIn` operators that convert `Flows` into `StateFlows` (`hasUnreadChannels`, `hasUnreadPrivateMessages`), ensuring they operate within the lifecycle of the provided scope.
- **`ChatViewModel.kt`**: The `viewModelScope` is now passed to the `ChatState` constructor during its instantiation. This ties the lifecycle of the state's coroutines directly to the `ViewModel`'s lifecycle.
* Test: Use `TestScope` for coroutines in `CommandProcessorTest`
This commit refactors `CommandProcessorTest` to use a `TestScope` and `UnconfinedTestDispatcher` for managing coroutines.
This ensures that coroutine-based operations within the test are executed in a controlled and predictable manner, improving test reliability. The `coroutineScope` for `CommandProcessor` and the `scope` for `ChatState` are now both configured to use this test-specific scope.
---------
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -29,7 +29,9 @@ import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.nostr.NostrProofOfWork
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.core.ui.component.button.CloseButton
|
||||
import com.bitchat.android.net.TorMode
|
||||
import com.bitchat.android.net.TorPreferenceManager
|
||||
import com.bitchat.android.net.ArtiTorManager
|
||||
@@ -243,7 +245,7 @@ fun AboutSheet(
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 24.dp, bottom = 8.dp)
|
||||
)
|
||||
val themePref by com.bitchat.android.ui.theme.ThemePreferenceManager.themeFlow.collectAsState()
|
||||
val themePref by com.bitchat.android.ui.theme.ThemePreferenceManager.themeFlow.collectAsStateWithLifecycle()
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
@@ -279,8 +281,8 @@ fun AboutSheet(
|
||||
PoWPreferenceManager.init(context)
|
||||
}
|
||||
|
||||
val powEnabled by PoWPreferenceManager.powEnabled.collectAsState()
|
||||
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState()
|
||||
val powEnabled by PoWPreferenceManager.powEnabled.collectAsStateWithLifecycle()
|
||||
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
@@ -595,18 +597,12 @@ fun AboutSheet(
|
||||
.height(64.dp)
|
||||
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
|
||||
) {
|
||||
TextButton(
|
||||
CloseButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.close_plain),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -30,6 +29,7 @@ import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.core.ui.utils.singleOrTripleClickable
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
||||
/**
|
||||
* Header components for ChatScreen
|
||||
@@ -248,10 +248,10 @@ fun ChatHeaderContent(
|
||||
when {
|
||||
selectedPrivatePeer != null -> {
|
||||
// Private chat header - Fully reactive state tracking
|
||||
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
||||
val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
|
||||
val peerSessionStates by viewModel.peerSessionStates.observeAsState(emptyMap())
|
||||
val peerNicknames by viewModel.peerNicknames.observeAsState(emptyMap())
|
||||
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
||||
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
||||
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
||||
|
||||
// Reactive favorite computation - no more static lookups!
|
||||
val isFavorite = isFavoriteReactive(
|
||||
@@ -264,8 +264,8 @@ fun ChatHeaderContent(
|
||||
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, isFav=$isFavorite, sessionState=$sessionState")
|
||||
|
||||
// Pass geohash context and people for NIP-17 chat title formatting
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val geohashPeople by viewModel.geohashPeople.observeAsState(emptyList())
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||
|
||||
PrivateChatHeader(
|
||||
peerID = selectedPrivatePeer,
|
||||
@@ -523,18 +523,18 @@ private fun MainHeader(
|
||||
viewModel: ChatViewModel
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
||||
val joinedChannels by viewModel.joinedChannels.observeAsState(emptySet())
|
||||
val hasUnreadChannels by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
||||
val isConnected by viewModel.isConnected.observeAsState(false)
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val geohashPeople by viewModel.geohashPeople.observeAsState(emptyList())
|
||||
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||
val joinedChannels by viewModel.joinedChannels.collectAsStateWithLifecycle()
|
||||
val hasUnreadChannels by viewModel.unreadChannelMessages.collectAsStateWithLifecycle()
|
||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
val isConnected by viewModel.isConnected.collectAsStateWithLifecycle()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||
|
||||
// Bookmarks store for current geohash toggle (iOS parity)
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val bookmarksStore = remember { com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(context) }
|
||||
val bookmarks by bookmarksStore.bookmarks.observeAsState(emptyList())
|
||||
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -653,8 +653,8 @@ private fun LocationChannelsButton(
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
// Get current channel selection from location manager
|
||||
val selectedChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val teleported by viewModel.isTeleported.observeAsState(false)
|
||||
val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val teleported by viewModel.isTeleported.collectAsStateWithLifecycle()
|
||||
|
||||
val (badgeText, badgeColor) = when (selectedChannel) {
|
||||
is com.bitchat.android.geohash.ChannelID.Mesh -> {
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -25,6 +24,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.ui.media.FullScreenImageViewer
|
||||
|
||||
@@ -41,22 +41,22 @@ import com.bitchat.android.ui.media.FullScreenImageViewer
|
||||
@Composable
|
||||
fun ChatScreen(viewModel: ChatViewModel) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val messages by viewModel.messages.observeAsState(emptyList())
|
||||
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
||||
val nickname by viewModel.nickname.observeAsState("")
|
||||
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.observeAsState()
|
||||
val currentChannel by viewModel.currentChannel.observeAsState()
|
||||
val joinedChannels by viewModel.joinedChannels.observeAsState(emptySet())
|
||||
val hasUnreadChannels by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
||||
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
||||
val channelMessages by viewModel.channelMessages.observeAsState(emptyMap())
|
||||
val showSidebar by viewModel.showSidebar.observeAsState(false)
|
||||
val showCommandSuggestions by viewModel.showCommandSuggestions.observeAsState(false)
|
||||
val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
|
||||
val showMentionSuggestions by viewModel.showMentionSuggestions.observeAsState(false)
|
||||
val mentionSuggestions by viewModel.mentionSuggestions.observeAsState(emptyList())
|
||||
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
|
||||
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
|
||||
val currentChannel by viewModel.currentChannel.collectAsStateWithLifecycle()
|
||||
val joinedChannels by viewModel.joinedChannels.collectAsStateWithLifecycle()
|
||||
val hasUnreadChannels by viewModel.unreadChannelMessages.collectAsStateWithLifecycle()
|
||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
||||
val channelMessages by viewModel.channelMessages.collectAsStateWithLifecycle()
|
||||
val showSidebar by viewModel.showSidebar.collectAsStateWithLifecycle()
|
||||
val showCommandSuggestions by viewModel.showCommandSuggestions.collectAsStateWithLifecycle()
|
||||
val commandSuggestions by viewModel.commandSuggestions.collectAsStateWithLifecycle()
|
||||
val showMentionSuggestions by viewModel.showMentionSuggestions.collectAsStateWithLifecycle()
|
||||
val mentionSuggestions by viewModel.mentionSuggestions.collectAsStateWithLifecycle()
|
||||
val showAppInfo by viewModel.showAppInfo.collectAsStateWithLifecycle()
|
||||
|
||||
var messageText by remember { mutableStateOf(TextFieldValue("")) }
|
||||
var showPasswordPrompt by remember { mutableStateOf(false) }
|
||||
@@ -78,11 +78,11 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
showPasswordDialog = showPasswordPrompt
|
||||
}
|
||||
|
||||
val isConnected by viewModel.isConnected.observeAsState(false)
|
||||
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)
|
||||
val isConnected by viewModel.isConnected.collectAsStateWithLifecycle()
|
||||
val passwordPromptChannel by viewModel.passwordPromptChannel.collectAsStateWithLifecycle()
|
||||
|
||||
// Get location channel info for timeline switching
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
|
||||
// Determine what messages to show based on current context (unified timelines)
|
||||
val displayMessages = when {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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
|
||||
@@ -21,171 +28,162 @@ data class CommandSuggestion(
|
||||
/**
|
||||
* Contains all the observable state for the chat system
|
||||
*/
|
||||
class ChatState {
|
||||
class ChatState(
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
|
||||
// Core messages and peer state
|
||||
private val _messages = MutableLiveData<List<BitchatMessage>>(emptyList())
|
||||
val messages: LiveData<List<BitchatMessage>> = _messages
|
||||
private val _messages = MutableStateFlow<List<BitchatMessage>>(emptyList())
|
||||
val messages: StateFlow<List<BitchatMessage>> = _messages.asStateFlow()
|
||||
|
||||
private val _connectedPeers = MutableLiveData<List<String>>(emptyList())
|
||||
val connectedPeers: LiveData<List<String>> = _connectedPeers
|
||||
private val _connectedPeers = MutableStateFlow<List<String>>(emptyList())
|
||||
val connectedPeers: StateFlow<List<String>> = _connectedPeers.asStateFlow()
|
||||
|
||||
private val _nickname = MutableLiveData<String>()
|
||||
val nickname: LiveData<String> = _nickname
|
||||
private val _nickname = MutableStateFlow<String>("")
|
||||
val nickname: StateFlow<String> = _nickname.asStateFlow()
|
||||
|
||||
private val _isConnected = MutableLiveData<Boolean>(false)
|
||||
val isConnected: LiveData<Boolean> = _isConnected
|
||||
private val _isConnected = MutableStateFlow<Boolean>(false)
|
||||
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
|
||||
|
||||
// Private chats
|
||||
private val _privateChats = MutableLiveData<Map<String, List<BitchatMessage>>>(emptyMap())
|
||||
val privateChats: LiveData<Map<String, List<BitchatMessage>>> = _privateChats
|
||||
private val _privateChats = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
|
||||
val privateChats: StateFlow<Map<String, List<BitchatMessage>>> = _privateChats.asStateFlow()
|
||||
|
||||
private val _selectedPrivateChatPeer = MutableLiveData<String?>(null)
|
||||
val selectedPrivateChatPeer: LiveData<String?> = _selectedPrivateChatPeer
|
||||
private val _selectedPrivateChatPeer = MutableStateFlow<String?>(null)
|
||||
val selectedPrivateChatPeer: StateFlow<String?> = _selectedPrivateChatPeer.asStateFlow()
|
||||
|
||||
private val _unreadPrivateMessages = MutableLiveData<Set<String>>(emptySet())
|
||||
val unreadPrivateMessages: LiveData<Set<String>> = _unreadPrivateMessages
|
||||
private val _unreadPrivateMessages = MutableStateFlow<Set<String>>(emptySet())
|
||||
val unreadPrivateMessages: StateFlow<Set<String>> = _unreadPrivateMessages.asStateFlow()
|
||||
|
||||
// Channels
|
||||
private val _joinedChannels = MutableLiveData<Set<String>>(emptySet())
|
||||
val joinedChannels: LiveData<Set<String>> = _joinedChannels
|
||||
private val _joinedChannels = MutableStateFlow<Set<String>>(emptySet())
|
||||
val joinedChannels: StateFlow<Set<String>> = _joinedChannels.asStateFlow()
|
||||
|
||||
private val _currentChannel = MutableLiveData<String?>(null)
|
||||
val currentChannel: LiveData<String?> = _currentChannel
|
||||
private val _currentChannel = MutableStateFlow<String?>(null)
|
||||
val currentChannel: StateFlow<String?> = _currentChannel.asStateFlow()
|
||||
|
||||
private val _channelMessages = MutableLiveData<Map<String, List<BitchatMessage>>>(emptyMap())
|
||||
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = _channelMessages
|
||||
private val _channelMessages = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
|
||||
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = _channelMessages.asStateFlow()
|
||||
|
||||
private val _unreadChannelMessages = MutableLiveData<Map<String, Int>>(emptyMap())
|
||||
val unreadChannelMessages: LiveData<Map<String, Int>> = _unreadChannelMessages
|
||||
private val _unreadChannelMessages = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||
val unreadChannelMessages: StateFlow<Map<String, Int>> = _unreadChannelMessages.asStateFlow()
|
||||
|
||||
private val _passwordProtectedChannels = MutableLiveData<Set<String>>(emptySet())
|
||||
val passwordProtectedChannels: LiveData<Set<String>> = _passwordProtectedChannels
|
||||
private val _passwordProtectedChannels = MutableStateFlow<Set<String>>(emptySet())
|
||||
val passwordProtectedChannels: StateFlow<Set<String>> = _passwordProtectedChannels.asStateFlow()
|
||||
|
||||
private val _showPasswordPrompt = MutableLiveData<Boolean>(false)
|
||||
val showPasswordPrompt: LiveData<Boolean> = _showPasswordPrompt
|
||||
private val _showPasswordPrompt = MutableStateFlow<Boolean>(false)
|
||||
val showPasswordPrompt: StateFlow<Boolean> = _showPasswordPrompt.asStateFlow()
|
||||
|
||||
private val _passwordPromptChannel = MutableLiveData<String?>(null)
|
||||
val passwordPromptChannel: LiveData<String?> = _passwordPromptChannel
|
||||
private val _passwordPromptChannel = MutableStateFlow<String?>(null)
|
||||
val passwordPromptChannel: StateFlow<String?> = _passwordPromptChannel.asStateFlow()
|
||||
|
||||
// Sidebar state
|
||||
private val _showSidebar = MutableLiveData(false)
|
||||
val showSidebar: LiveData<Boolean> = _showSidebar
|
||||
private val _showSidebar = MutableStateFlow(false)
|
||||
val showSidebar: StateFlow<Boolean> = _showSidebar.asStateFlow()
|
||||
|
||||
// Command autocomplete
|
||||
private val _showCommandSuggestions = MutableLiveData(false)
|
||||
val showCommandSuggestions: LiveData<Boolean> = _showCommandSuggestions
|
||||
private val _showCommandSuggestions = MutableStateFlow(false)
|
||||
val showCommandSuggestions: StateFlow<Boolean> = _showCommandSuggestions.asStateFlow()
|
||||
|
||||
private val _commandSuggestions = MutableLiveData<List<CommandSuggestion>>(emptyList())
|
||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = _commandSuggestions
|
||||
private val _commandSuggestions = MutableStateFlow<List<CommandSuggestion>>(emptyList())
|
||||
val commandSuggestions: StateFlow<List<CommandSuggestion>> = _commandSuggestions.asStateFlow()
|
||||
|
||||
// Mention autocomplete
|
||||
private val _showMentionSuggestions = MutableLiveData(false)
|
||||
val showMentionSuggestions: LiveData<Boolean> = _showMentionSuggestions
|
||||
private val _showMentionSuggestions = MutableStateFlow(false)
|
||||
val showMentionSuggestions: StateFlow<Boolean> = _showMentionSuggestions.asStateFlow()
|
||||
|
||||
private val _mentionSuggestions = MutableLiveData<List<String>>(emptyList())
|
||||
val mentionSuggestions: LiveData<List<String>> = _mentionSuggestions
|
||||
private val _mentionSuggestions = MutableStateFlow<List<String>>(emptyList())
|
||||
val mentionSuggestions: StateFlow<List<String>> = _mentionSuggestions.asStateFlow()
|
||||
|
||||
// Favorites
|
||||
private val _favoritePeers = MutableLiveData<Set<String>>(emptySet())
|
||||
val favoritePeers: LiveData<Set<String>> = _favoritePeers
|
||||
private val _favoritePeers = MutableStateFlow<Set<String>>(emptySet())
|
||||
val favoritePeers: StateFlow<Set<String>> = _favoritePeers.asStateFlow()
|
||||
|
||||
// Noise session states for peers (for reactive UI updates)
|
||||
private val _peerSessionStates = MutableLiveData<Map<String, String>>(emptyMap())
|
||||
val peerSessionStates: LiveData<Map<String, String>> = _peerSessionStates
|
||||
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 = MutableLiveData<Map<String, String>>(emptyMap())
|
||||
val peerFingerprints: LiveData<Map<String, String>> = _peerFingerprints
|
||||
private val _peerFingerprints = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||
val peerFingerprints: StateFlow<Map<String, String>> = _peerFingerprints.asStateFlow()
|
||||
|
||||
private val _peerNicknames = MutableLiveData<Map<String, String>>(emptyMap())
|
||||
val peerNicknames: LiveData<Map<String, String>> = _peerNicknames
|
||||
private val _peerNicknames = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||
val peerNicknames: StateFlow<Map<String, String>> = _peerNicknames.asStateFlow()
|
||||
|
||||
private val _peerRSSI = MutableLiveData<Map<String, Int>>(emptyMap())
|
||||
val peerRSSI: LiveData<Map<String, Int>> = _peerRSSI
|
||||
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 = MutableLiveData<Map<String, Boolean>>(emptyMap())
|
||||
val peerDirect: LiveData<Map<String, Boolean>> = _peerDirect
|
||||
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 = MutableLiveData<Boolean>(false)
|
||||
val showAppInfo: LiveData<Boolean> = _showAppInfo
|
||||
private val _showAppInfo = MutableStateFlow<Boolean>(false)
|
||||
val showAppInfo: StateFlow<Boolean> = _showAppInfo.asStateFlow()
|
||||
|
||||
// Location channels state (for Nostr geohash features)
|
||||
private val _selectedLocationChannel = MutableLiveData<com.bitchat.android.geohash.ChannelID?>(com.bitchat.android.geohash.ChannelID.Mesh)
|
||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = _selectedLocationChannel
|
||||
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 = MutableLiveData<Boolean>(false)
|
||||
val isTeleported: LiveData<Boolean> = _isTeleported
|
||||
private val _isTeleported = MutableStateFlow<Boolean>(false)
|
||||
val isTeleported: StateFlow<Boolean> = _isTeleported.asStateFlow()
|
||||
|
||||
// Geohash people state (iOS-compatible)
|
||||
private val _geohashPeople = MutableLiveData<List<GeoPerson>>(emptyList())
|
||||
val geohashPeople: LiveData<List<GeoPerson>> = _geohashPeople
|
||||
// For background thread updates by repositories/handlers in their own scopes
|
||||
val geohashPeopleMutable: MutableLiveData<List<GeoPerson>> get() = _geohashPeople
|
||||
private val _geohashPeople = MutableStateFlow<List<GeoPerson>>(emptyList())
|
||||
val geohashPeople: StateFlow<List<GeoPerson>> = _geohashPeople.asStateFlow()
|
||||
|
||||
|
||||
private val _teleportedGeo = MutableLiveData<Set<String>>(emptySet())
|
||||
val teleportedGeo: LiveData<Set<String>> = _teleportedGeo
|
||||
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 = MutableLiveData<Map<String, Int>>(emptyMap())
|
||||
val geohashParticipantCounts: LiveData<Map<String, Int>> = _geohashParticipantCounts
|
||||
private val _geohashParticipantCounts = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||
val geohashParticipantCounts: StateFlow<Map<String, Int>> = _geohashParticipantCounts.asStateFlow()
|
||||
|
||||
// Unread state computed properties
|
||||
val hasUnreadChannels: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
||||
val hasUnreadPrivateMessages: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
||||
|
||||
val hasUnreadChannels: StateFlow<Boolean> = _unreadChannelMessages
|
||||
.map { unreadMap -> unreadMap.values.any { it > 0 } }
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = WhileSubscribed(5_000),
|
||||
initialValue = false
|
||||
)
|
||||
|
||||
init {
|
||||
// Initialize unread state mediators
|
||||
hasUnreadChannels.addSource(_unreadChannelMessages) { unreadMap ->
|
||||
hasUnreadChannels.value = unreadMap.values.any { it > 0 }
|
||||
}
|
||||
|
||||
hasUnreadPrivateMessages.addSource(_unreadPrivateMessages) { unreadSet ->
|
||||
hasUnreadPrivateMessages.value = unreadSet.isNotEmpty()
|
||||
}
|
||||
}
|
||||
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 ?: emptyList()
|
||||
fun getConnectedPeersValue() = _connectedPeers.value ?: emptyList()
|
||||
fun getMessagesValue() = _messages.value
|
||||
fun getConnectedPeersValue() = _connectedPeers.value
|
||||
fun getNicknameValue() = _nickname.value
|
||||
fun getPrivateChatsValue() = _privateChats.value ?: emptyMap()
|
||||
fun getPrivateChatsValue() = _privateChats.value
|
||||
fun getSelectedPrivateChatPeerValue() = _selectedPrivateChatPeer.value
|
||||
fun getUnreadPrivateMessagesValue() = _unreadPrivateMessages.value ?: emptySet()
|
||||
fun getJoinedChannelsValue() = _joinedChannels.value ?: emptySet()
|
||||
// Thread-safe posting helpers for background updates
|
||||
fun postGeohashPeople(people: List<GeoPerson>) {
|
||||
_geohashPeople.postValue(people)
|
||||
}
|
||||
|
||||
fun postGeohashParticipantCounts(counts: Map<String, Int>) {
|
||||
_geohashParticipantCounts.postValue(counts)
|
||||
}
|
||||
|
||||
|
||||
fun getUnreadPrivateMessagesValue() = _unreadPrivateMessages.value
|
||||
fun getJoinedChannelsValue() = _joinedChannels.value
|
||||
fun getCurrentChannelValue() = _currentChannel.value
|
||||
fun getChannelMessagesValue() = _channelMessages.value ?: emptyMap()
|
||||
fun getUnreadChannelMessagesValue() = _unreadChannelMessages.value ?: emptyMap()
|
||||
fun getPasswordProtectedChannelsValue() = _passwordProtectedChannels.value ?: emptySet()
|
||||
fun getShowPasswordPromptValue() = _showPasswordPrompt.value ?: false
|
||||
fun getChannelMessagesValue() = _channelMessages.value
|
||||
fun getUnreadChannelMessagesValue() = _unreadChannelMessages.value
|
||||
fun getPasswordProtectedChannelsValue() = _passwordProtectedChannels.value
|
||||
fun getShowPasswordPromptValue() = _showPasswordPrompt.value
|
||||
fun getPasswordPromptChannelValue() = _passwordPromptChannel.value
|
||||
fun getShowSidebarValue() = _showSidebar.value ?: false
|
||||
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
|
||||
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
|
||||
fun getShowMentionSuggestionsValue() = _showMentionSuggestions.value ?: false
|
||||
fun getMentionSuggestionsValue() = _mentionSuggestions.value ?: emptyList()
|
||||
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
|
||||
fun getPeerSessionStatesValue() = _peerSessionStates.value ?: emptyMap()
|
||||
fun getPeerFingerprintsValue() = _peerFingerprints.value ?: emptyMap()
|
||||
fun getShowAppInfoValue() = _showAppInfo.value ?: false
|
||||
fun getGeohashPeopleValue() = _geohashPeople.value ?: emptyList()
|
||||
fun getTeleportedGeoValue() = _teleportedGeo.value ?: emptySet()
|
||||
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value ?: emptyMap()
|
||||
fun getShowSidebarValue() = _showSidebar.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 getTeleportedGeoValue() = _teleportedGeo.value
|
||||
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value
|
||||
|
||||
// Setters for state updates
|
||||
fun setMessages(messages: List<BitchatMessage>) {
|
||||
@@ -197,7 +195,7 @@ class ChatState {
|
||||
}
|
||||
|
||||
fun postTeleportedGeo(teleported: Set<String>) {
|
||||
_teleportedGeo.postValue(teleported)
|
||||
_teleportedGeo.value = teleported
|
||||
}
|
||||
|
||||
fun setNickname(nickname: String) {
|
||||
@@ -269,7 +267,7 @@ class ChatState {
|
||||
}
|
||||
|
||||
fun setFavoritePeers(favorites: Set<String>) {
|
||||
val currentValue = _favoritePeers.value ?: emptySet()
|
||||
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}")
|
||||
@@ -278,8 +276,7 @@ class ChatState {
|
||||
// Always set the value - even if equal, this ensures observers are triggered
|
||||
_favoritePeers.value = favorites
|
||||
|
||||
Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}")
|
||||
Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}")
|
||||
Log.d("ChatState", "StateFlow value after set: ${_favoritePeers.value}")
|
||||
}
|
||||
|
||||
fun setPeerSessionStates(states: Map<String, String>) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
@@ -47,7 +47,9 @@ class ChatViewModel(
|
||||
}
|
||||
|
||||
// MARK: - State management
|
||||
private val state = ChatState()
|
||||
private val state = ChatState(
|
||||
scope = viewModelScope,
|
||||
)
|
||||
|
||||
// Transfer progress tracking
|
||||
private val transferMessageMap = mutableMapOf<String, String>()
|
||||
@@ -103,40 +105,40 @@ class ChatViewModel(
|
||||
|
||||
|
||||
|
||||
// Expose state through LiveData (maintaining the same interface)
|
||||
val messages: LiveData<List<BitchatMessage>> = state.messages
|
||||
val connectedPeers: LiveData<List<String>> = state.connectedPeers
|
||||
val nickname: LiveData<String> = state.nickname
|
||||
val isConnected: LiveData<Boolean> = state.isConnected
|
||||
val privateChats: LiveData<Map<String, List<BitchatMessage>>> = state.privateChats
|
||||
val selectedPrivateChatPeer: LiveData<String?> = state.selectedPrivateChatPeer
|
||||
val unreadPrivateMessages: LiveData<Set<String>> = state.unreadPrivateMessages
|
||||
val joinedChannels: LiveData<Set<String>> = state.joinedChannels
|
||||
val currentChannel: LiveData<String?> = state.currentChannel
|
||||
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = state.channelMessages
|
||||
val unreadChannelMessages: LiveData<Map<String, Int>> = state.unreadChannelMessages
|
||||
val passwordProtectedChannels: LiveData<Set<String>> = state.passwordProtectedChannels
|
||||
val showPasswordPrompt: LiveData<Boolean> = state.showPasswordPrompt
|
||||
val passwordPromptChannel: LiveData<String?> = state.passwordPromptChannel
|
||||
val showSidebar: LiveData<Boolean> = state.showSidebar
|
||||
|
||||
val messages: StateFlow<List<BitchatMessage>> = state.messages
|
||||
val connectedPeers: StateFlow<List<String>> = state.connectedPeers
|
||||
val nickname: StateFlow<String> = state.nickname
|
||||
val isConnected: StateFlow<Boolean> = state.isConnected
|
||||
val privateChats: StateFlow<Map<String, List<BitchatMessage>>> = state.privateChats
|
||||
val selectedPrivateChatPeer: StateFlow<String?> = state.selectedPrivateChatPeer
|
||||
val unreadPrivateMessages: StateFlow<Set<String>> = state.unreadPrivateMessages
|
||||
val joinedChannels: StateFlow<Set<String>> = state.joinedChannels
|
||||
val currentChannel: StateFlow<String?> = state.currentChannel
|
||||
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = state.channelMessages
|
||||
val unreadChannelMessages: StateFlow<Map<String, Int>> = state.unreadChannelMessages
|
||||
val passwordProtectedChannels: StateFlow<Set<String>> = state.passwordProtectedChannels
|
||||
val showPasswordPrompt: StateFlow<Boolean> = state.showPasswordPrompt
|
||||
val passwordPromptChannel: StateFlow<String?> = state.passwordPromptChannel
|
||||
val showSidebar: StateFlow<Boolean> = state.showSidebar
|
||||
val hasUnreadChannels = state.hasUnreadChannels
|
||||
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
|
||||
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
||||
val showMentionSuggestions: LiveData<Boolean> = state.showMentionSuggestions
|
||||
val mentionSuggestions: LiveData<List<String>> = state.mentionSuggestions
|
||||
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
||||
val peerSessionStates: LiveData<Map<String, String>> = state.peerSessionStates
|
||||
val peerFingerprints: LiveData<Map<String, String>> = state.peerFingerprints
|
||||
val peerNicknames: LiveData<Map<String, String>> = state.peerNicknames
|
||||
val peerRSSI: LiveData<Map<String, Int>> = state.peerRSSI
|
||||
val peerDirect: LiveData<Map<String, Boolean>> = state.peerDirect
|
||||
val showAppInfo: LiveData<Boolean> = state.showAppInfo
|
||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||
val isTeleported: LiveData<Boolean> = state.isTeleported
|
||||
val geohashPeople: LiveData<List<GeoPerson>> = state.geohashPeople
|
||||
val teleportedGeo: LiveData<Set<String>> = state.teleportedGeo
|
||||
val geohashParticipantCounts: LiveData<Map<String, Int>> = state.geohashParticipantCounts
|
||||
val showCommandSuggestions: StateFlow<Boolean> = state.showCommandSuggestions
|
||||
val commandSuggestions: StateFlow<List<CommandSuggestion>> = state.commandSuggestions
|
||||
val showMentionSuggestions: StateFlow<Boolean> = state.showMentionSuggestions
|
||||
val mentionSuggestions: StateFlow<List<String>> = state.mentionSuggestions
|
||||
val favoritePeers: StateFlow<Set<String>> = state.favoritePeers
|
||||
val peerSessionStates: StateFlow<Map<String, String>> = state.peerSessionStates
|
||||
val peerFingerprints: StateFlow<Map<String, String>> = state.peerFingerprints
|
||||
val peerNicknames: StateFlow<Map<String, String>> = state.peerNicknames
|
||||
val peerRSSI: StateFlow<Map<String, Int>> = state.peerRSSI
|
||||
val peerDirect: StateFlow<Map<String, Boolean>> = state.peerDirect
|
||||
val showAppInfo: StateFlow<Boolean> = state.showAppInfo
|
||||
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||
val isTeleported: StateFlow<Boolean> = state.isTeleported
|
||||
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
||||
val teleportedGeo: StateFlow<Set<String>> = state.teleportedGeo
|
||||
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
||||
|
||||
init {
|
||||
// Note: Mesh service delegate is now set by MainActivity
|
||||
@@ -565,7 +567,7 @@ class ChatViewModel(
|
||||
|
||||
private fun logCurrentFavoriteState() {
|
||||
Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===")
|
||||
Log.i("ChatViewModel", "LiveData favorite peers: ${favoritePeers.value}")
|
||||
Log.i("ChatViewModel", "StateFlow favorite peers: ${favoritePeers.value}")
|
||||
Log.i("ChatViewModel", "DataManager favorite peers: ${dataManager.favoritePeers}")
|
||||
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
|
||||
Log.i("ChatViewModel", "==============================")
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.outlined.Explore
|
||||
import androidx.compose.material.icons.outlined.LocationOn
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -21,6 +20,7 @@ import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import java.util.*
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
|
||||
/**
|
||||
@@ -46,11 +46,11 @@ fun GeohashPeopleList(
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
// Observe geohash people from ChatViewModel
|
||||
val geohashPeople by viewModel.geohashPeople.observeAsState(emptyList())
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val isTeleported by viewModel.isTeleported.observeAsState(false)
|
||||
val nickname by viewModel.nickname.observeAsState("")
|
||||
val unreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
||||
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val isTeleported by viewModel.isTeleported.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
|
||||
Column {
|
||||
// Header matching iOS style
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.bitchat.android.ui
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitchat.android.nostr.GeohashMessageHandler
|
||||
import com.bitchat.android.nostr.GeohashRepository
|
||||
@@ -15,6 +14,7 @@ import com.bitchat.android.nostr.NostrSubscriptionManager
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
|
||||
@@ -55,9 +55,9 @@ class GeohashViewModel(
|
||||
private var geoTimer: Job? = null
|
||||
private var locationChannelManager: com.bitchat.android.geohash.LocationChannelManager? = null
|
||||
|
||||
val geohashPeople: LiveData<List<GeoPerson>> = state.geohashPeople
|
||||
val geohashParticipantCounts: LiveData<Map<String, Int>> = state.geohashParticipantCounts
|
||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
||||
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
||||
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||
|
||||
fun initialize() {
|
||||
subscriptionManager.connect()
|
||||
@@ -73,12 +73,16 @@ class GeohashViewModel(
|
||||
}
|
||||
try {
|
||||
locationChannelManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
|
||||
locationChannelManager?.selectedChannel?.observeForever { channel ->
|
||||
state.setSelectedLocationChannel(channel)
|
||||
switchLocationChannel(channel)
|
||||
viewModelScope.launch {
|
||||
locationChannelManager?.selectedChannel?.collect { channel ->
|
||||
state.setSelectedLocationChannel(channel)
|
||||
switchLocationChannel(channel)
|
||||
}
|
||||
}
|
||||
locationChannelManager?.teleported?.observeForever { teleported ->
|
||||
state.setIsTeleported(teleported)
|
||||
viewModelScope.launch {
|
||||
locationChannelManager?.teleported?.collect { teleported ->
|
||||
state.setIsTeleported(teleported)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize location channel state: ${e.message}")
|
||||
@@ -120,7 +124,7 @@ class GeohashViewModel(
|
||||
}
|
||||
try {
|
||||
val identity = NostrIdentityBridge.deriveIdentity(forGeohash = channel.geohash, context = getApplication())
|
||||
val teleported = state.isTeleported.value ?: false
|
||||
val teleported = state.isTeleported.value
|
||||
val event = NostrProtocol.createEphemeralGeohashEvent(content, channel.geohash, identity, nickname, teleported)
|
||||
val relayManager = NostrRelayManager.getInstance(getApplication())
|
||||
relayManager.sendEventToGeohash(event, channel.geohash, includeDefaults = false, nRelays = 5)
|
||||
@@ -231,7 +235,7 @@ class GeohashViewModel(
|
||||
try {
|
||||
val identity = NostrIdentityBridge.deriveIdentity(channel.channel.geohash, getApplication())
|
||||
repo.updateParticipant(channel.channel.geohash, identity.publicKeyHex, Date())
|
||||
val teleported = state.isTeleported.value ?: false
|
||||
val teleported = state.isTeleported.value
|
||||
if (teleported) repo.markTeleported(identity.publicKeyHex)
|
||||
} catch (e: Exception) { Log.w(TAG, "Failed identity setup: ${e.message}") }
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.material.icons.filled.PinDrop
|
||||
import androidx.compose.material.icons.outlined.BookmarkBorder
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
@@ -38,7 +37,9 @@ import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.geohash.GeohashBookmarksStore
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.core.ui.component.button.CloseButton
|
||||
|
||||
/**
|
||||
* Location Channels Sheet for selecting geohash-based location channels
|
||||
@@ -57,18 +58,18 @@ fun LocationChannelsSheet(
|
||||
val bookmarksStore = remember { GeohashBookmarksStore.getInstance(context) }
|
||||
|
||||
// Observe location manager state
|
||||
val permissionState by locationManager.permissionState.observeAsState()
|
||||
val availableChannels by locationManager.availableChannels.observeAsState(emptyList())
|
||||
val selectedChannel by locationManager.selectedChannel.observeAsState()
|
||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
||||
val locationServicesEnabled by locationManager.locationServicesEnabled.observeAsState(false)
|
||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||
val selectedChannel by locationManager.selectedChannel.collectAsStateWithLifecycle()
|
||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||
val locationServicesEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle()
|
||||
|
||||
// Observe bookmarks state
|
||||
val bookmarks by bookmarksStore.bookmarks.observeAsState(emptyList())
|
||||
val bookmarkNames by bookmarksStore.bookmarkNames.observeAsState(emptyMap())
|
||||
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
|
||||
val bookmarkNames by bookmarksStore.bookmarkNames.collectAsStateWithLifecycle()
|
||||
|
||||
// Observe reactive participant counts
|
||||
val geohashParticipantCounts by viewModel.geohashParticipantCounts.observeAsState(emptyMap())
|
||||
val geohashParticipantCounts by viewModel.geohashParticipantCounts.collectAsStateWithLifecycle()
|
||||
|
||||
// UI state
|
||||
var customGeohash by remember { mutableStateOf("") }
|
||||
@@ -551,18 +552,12 @@ fun LocationChannelsSheet(
|
||||
.height(56.dp)
|
||||
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
|
||||
) {
|
||||
TextButton(
|
||||
CloseButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.close_plain),
|
||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -35,10 +35,10 @@ fun LocationNotesButton(
|
||||
val context = LocalContext.current
|
||||
|
||||
// Get channel and permission state
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
val permissionState by locationManager.permissionState.observeAsState()
|
||||
val locationServicesEnabled by locationManager.locationServicesEnabled.observeAsState(false)
|
||||
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||
val locationServicesEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle(false)
|
||||
|
||||
// Check both permission AND location services enabled
|
||||
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
|
||||
@@ -46,7 +46,7 @@ fun LocationNotesButton(
|
||||
|
||||
// Get notes count from LocationNotesManager
|
||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||
val notes by notesManager.notes.observeAsState(emptyList())
|
||||
val notes by notesManager.notes.collectAsStateWithLifecycle()
|
||||
val notesCount = notes.size
|
||||
|
||||
// Only show in mesh mode when location is authorized (iOS pattern)
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -25,6 +24,7 @@ import com.bitchat.android.R
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
@@ -57,16 +57,16 @@ fun LocationNotesSheet(
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
|
||||
// State
|
||||
val notes by notesManager.notes.observeAsState(emptyList())
|
||||
val state by notesManager.state.observeAsState(LocationNotesManager.State.IDLE)
|
||||
val errorMessage by notesManager.errorMessage.observeAsState()
|
||||
val initialLoadComplete by notesManager.initialLoadComplete.observeAsState(false)
|
||||
val notes by notesManager.notes.collectAsStateWithLifecycle()
|
||||
val state by notesManager.state.collectAsStateWithLifecycle(LocationNotesManager.State.IDLE)
|
||||
val errorMessage by notesManager.errorMessage.collectAsStateWithLifecycle()
|
||||
val initialLoadComplete by notesManager.initialLoadComplete.collectAsStateWithLifecycle(false)
|
||||
|
||||
// SIMPLIFIED: Get count directly from notes list (no separate counter needed)
|
||||
val count = notes.size
|
||||
|
||||
// Get location name (building or block) - matches iOS locationNames lookup
|
||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||
val displayLocationName = locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() }
|
||||
?: locationNames[GeohashChannelLevel.BLOCK]?.takeIf { it.isNotEmpty() }
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
|
||||
@@ -26,15 +26,15 @@ fun LocationNotesSheetPresenter(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
val availableChannels by locationManager.availableChannels.observeAsState(emptyList())
|
||||
val nickname by viewModel.nickname.observeAsState("")
|
||||
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
|
||||
// iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
||||
val buildingGeohash = availableChannels.firstOrNull { it.level == GeohashChannelLevel.BUILDING }?.geohash
|
||||
|
||||
if (buildingGeohash != null) {
|
||||
// Get location name from locationManager
|
||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
||||
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||
val locationName = locationNames[GeohashChannelLevel.BUILDING]
|
||||
?: locationNames[GeohashChannelLevel.BLOCK]
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -26,7 +27,7 @@ private enum class CharacterAnimationState {
|
||||
*/
|
||||
@Composable
|
||||
fun shouldAnimateMessage(messageId: String): Boolean {
|
||||
val miningMessages by PoWMiningTracker.miningMessages.collectAsState()
|
||||
val miningMessages by PoWMiningTracker.miningMessages.collectAsStateWithLifecycle()
|
||||
return miningMessages.contains(messageId)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.bitchat.android.nostr.NostrProofOfWork
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.R
|
||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||
|
||||
@@ -27,9 +28,9 @@ fun PoWStatusIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
style: PoWIndicatorStyle = PoWIndicatorStyle.COMPACT
|
||||
) {
|
||||
val powEnabled by PoWPreferenceManager.powEnabled.collectAsState()
|
||||
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState()
|
||||
val isMining by PoWPreferenceManager.isMining.collectAsState()
|
||||
val powEnabled by PoWPreferenceManager.powEnabled.collectAsStateWithLifecycle()
|
||||
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsStateWithLifecycle()
|
||||
val isMining by PoWPreferenceManager.isMining.collectAsStateWithLifecycle()
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -22,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||
|
||||
|
||||
@@ -39,14 +39,14 @@ fun SidebarOverlay(
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
||||
val joinedChannels by viewModel.joinedChannels.observeAsState(emptyList())
|
||||
val currentChannel by viewModel.currentChannel.observeAsState()
|
||||
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.observeAsState()
|
||||
val nickname by viewModel.nickname.observeAsState("")
|
||||
val unreadChannelMessages by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
||||
val peerNicknames by viewModel.peerNicknames.observeAsState(emptyMap())
|
||||
val peerRSSI by viewModel.peerRSSI.observeAsState(emptyMap())
|
||||
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||
val joinedChannels by viewModel.joinedChannels.collectAsStateWithLifecycle()
|
||||
val currentChannel by viewModel.currentChannel.collectAsStateWithLifecycle()
|
||||
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
|
||||
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||
val unreadChannelMessages by viewModel.unreadChannelMessages.collectAsStateWithLifecycle()
|
||||
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
||||
val peerRSSI by viewModel.peerRSSI.collectAsStateWithLifecycle()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -110,7 +110,7 @@ fun SidebarOverlay(
|
||||
|
||||
// People section - switch between mesh and geohash lists (iOS-compatible)
|
||||
item {
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
||||
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsState()
|
||||
|
||||
when (selectedLocationChannel) {
|
||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||
@@ -291,10 +291,10 @@ fun PeopleSection(
|
||||
}
|
||||
|
||||
// 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())
|
||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
||||
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
||||
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||
|
||||
// Reactive favorite computation for all peers
|
||||
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
|
||||
@@ -384,7 +384,7 @@ fun PeopleSection(
|
||||
val (bName, _) = com.bitchat.android.ui.splitSuffix(displayName)
|
||||
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
||||
|
||||
val directMap by viewModel.peerDirect.observeAsState(emptyMap())
|
||||
val directMap by viewModel.peerDirect.collectAsStateWithLifecycle()
|
||||
val isDirectLive = directMap[peerID] ?: try { viewModel.meshService.getPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false }
|
||||
PeerItem(
|
||||
peerID = peerID,
|
||||
|
||||
Reference in New Issue
Block a user