mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02: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:
@@ -0,0 +1,34 @@
|
|||||||
|
package com.bitchat.android.core.ui.component.button
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CloseButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier.Companion
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier
|
||||||
|
.size(32.dp),
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
|
||||||
|
containerColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Close",
|
||||||
|
modifier = Modifier.Companion.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,14 @@ import android.location.Geocoder
|
|||||||
import android.location.Location
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,11 +47,11 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
|
|
||||||
private val membership = mutableSetOf<String>()
|
private val membership = mutableSetOf<String>()
|
||||||
|
|
||||||
private val _bookmarks = MutableLiveData<List<String>>(emptyList())
|
private val _bookmarks = MutableStateFlow<List<String>>(emptyList())
|
||||||
val bookmarks: LiveData<List<String>> = _bookmarks
|
val bookmarks: StateFlow<List<String>> = _bookmarks.asStateFlow()
|
||||||
|
|
||||||
private val _bookmarkNames = MutableLiveData<Map<String, String>>(emptyMap())
|
private val _bookmarkNames = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val bookmarkNames: LiveData<Map<String, String>> = _bookmarkNames
|
val bookmarkNames: StateFlow<Map<String, String>> = _bookmarkNames.asStateFlow()
|
||||||
|
|
||||||
// For throttling / preventing duplicate geocode lookups
|
// For throttling / preventing duplicate geocode lookups
|
||||||
private val resolving = mutableSetOf<String>()
|
private val resolving = mutableSetOf<String>()
|
||||||
@@ -68,8 +69,8 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
val gh = normalize(geohash)
|
val gh = normalize(geohash)
|
||||||
if (gh.isEmpty() || membership.contains(gh)) return
|
if (gh.isEmpty() || membership.contains(gh)) return
|
||||||
membership.add(gh)
|
membership.add(gh)
|
||||||
val updated = listOf(gh) + (_bookmarks.value ?: emptyList())
|
val updated = listOf(gh) + (_bookmarks.value)
|
||||||
_bookmarks.postValue(updated)
|
_bookmarks.value = updated
|
||||||
persist(updated)
|
persist(updated)
|
||||||
// Resolve friendly name asynchronously
|
// Resolve friendly name asynchronously
|
||||||
resolveNameIfNeeded(gh)
|
resolveNameIfNeeded(gh)
|
||||||
@@ -79,12 +80,12 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
val gh = normalize(geohash)
|
val gh = normalize(geohash)
|
||||||
if (!membership.contains(gh)) return
|
if (!membership.contains(gh)) return
|
||||||
membership.remove(gh)
|
membership.remove(gh)
|
||||||
val updated = (_bookmarks.value ?: emptyList()).filterNot { it == gh }
|
val updated = (_bookmarks.value).filterNot { it == gh }
|
||||||
_bookmarks.postValue(updated)
|
_bookmarks.value = updated
|
||||||
// Remove stored name to avoid stale cache growth
|
// Remove stored name to avoid stale cache growth
|
||||||
val names = _bookmarkNames.value?.toMutableMap() ?: mutableMapOf()
|
val names = _bookmarkNames.value.toMutableMap()
|
||||||
if (names.remove(gh) != null) {
|
if (names.remove(gh) != null) {
|
||||||
_bookmarkNames.postValue(names)
|
_bookmarkNames.value = names
|
||||||
persistNames(names)
|
persistNames(names)
|
||||||
}
|
}
|
||||||
persist(updated)
|
persist(updated)
|
||||||
@@ -108,7 +109,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
membership.clear(); membership.addAll(seen)
|
membership.clear(); membership.addAll(seen)
|
||||||
_bookmarks.postValue(ordered)
|
_bookmarks.value = ordered
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load bookmarks: ${e.message}")
|
Log.e(TAG, "Failed to load bookmarks: ${e.message}")
|
||||||
@@ -118,7 +119,7 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
if (!namesJson.isNullOrEmpty()) {
|
if (!namesJson.isNullOrEmpty()) {
|
||||||
val mapType = object : TypeToken<Map<String, String>>() {}.type
|
val mapType = object : TypeToken<Map<String, String>>() {}.type
|
||||||
val dict = gson.fromJson<Map<String, String>>(namesJson, mapType)
|
val dict = gson.fromJson<Map<String, String>>(namesJson, mapType)
|
||||||
_bookmarkNames.postValue(dict)
|
_bookmarkNames.value = dict
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load bookmark names: ${e.message}")
|
Log.e(TAG, "Failed to load bookmark names: ${e.message}")
|
||||||
@@ -127,14 +128,14 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
|
|
||||||
private fun persist() {
|
private fun persist() {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(_bookmarks.value ?: emptyList<String>())
|
val json = gson.toJson(_bookmarks.value)
|
||||||
prefs.edit().putString(STORE_KEY, json).apply()
|
prefs.edit().putString(STORE_KEY, json).apply()
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistNames() {
|
private fun persistNames() {
|
||||||
try {
|
try {
|
||||||
val json = gson.toJson(_bookmarkNames.value ?: emptyMap<String, String>())
|
val json = gson.toJson(_bookmarkNames.value)
|
||||||
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
|
prefs.edit().putString(NAMES_STORE_KEY, json).apply()
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
@@ -144,8 +145,8 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
fun clearAll() {
|
fun clearAll() {
|
||||||
try {
|
try {
|
||||||
membership.clear()
|
membership.clear()
|
||||||
_bookmarks.postValue(emptyList())
|
_bookmarks.value = emptyList()
|
||||||
_bookmarkNames.postValue(emptyMap())
|
_bookmarkNames.value = emptyMap()
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.remove(STORE_KEY)
|
.remove(STORE_KEY)
|
||||||
.remove(NAMES_STORE_KEY)
|
.remove(NAMES_STORE_KEY)
|
||||||
@@ -209,9 +210,9 @@ class GeohashBookmarksStore private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!name.isNullOrEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
val current = _bookmarkNames.value?.toMutableMap() ?: mutableMapOf()
|
val current = _bookmarkNames.value.toMutableMap()
|
||||||
current[gh] = name
|
current[gh] = name
|
||||||
_bookmarkNames.postValue(current)
|
_bookmarkNames.value = current
|
||||||
persistNames(current)
|
persistNames(current)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import android.location.LocationManager
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonSyntaxException
|
import com.google.gson.JsonSyntaxException
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages location permissions, one-shot location retrieval, and computing geohash channels.
|
* Manages location permissions, one-shot location retrieval, and computing geohash channels.
|
||||||
@@ -53,28 +53,26 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private var dataManager: com.bitchat.android.ui.DataManager? = null
|
private var dataManager: com.bitchat.android.ui.DataManager? = null
|
||||||
|
|
||||||
// Published state for UI bindings (matching iOS @Published properties)
|
// Published state for UI bindings (matching iOS @Published properties)
|
||||||
private val _permissionState = MutableLiveData(PermissionState.NOT_DETERMINED)
|
private val _permissionState = MutableStateFlow(PermissionState.NOT_DETERMINED)
|
||||||
val permissionState: LiveData<PermissionState> = _permissionState
|
val permissionState: StateFlow<PermissionState> = _permissionState
|
||||||
|
|
||||||
private val _availableChannels = MutableLiveData<List<GeohashChannel>>(emptyList())
|
private val _availableChannels = MutableStateFlow<List<GeohashChannel>>(emptyList())
|
||||||
val availableChannels: LiveData<List<GeohashChannel>> = _availableChannels
|
val availableChannels: StateFlow<List<GeohashChannel>> = _availableChannels
|
||||||
|
|
||||||
private val _selectedChannel = MutableLiveData<ChannelID>(ChannelID.Mesh)
|
private val _selectedChannel = MutableStateFlow<ChannelID>(ChannelID.Mesh)
|
||||||
val selectedChannel: LiveData<ChannelID> = _selectedChannel
|
val selectedChannel: StateFlow<ChannelID> = _selectedChannel
|
||||||
|
|
||||||
private val _teleported = MutableLiveData(false)
|
private val _teleported = MutableStateFlow(false)
|
||||||
val teleported: LiveData<Boolean> = _teleported
|
val teleported: StateFlow<Boolean> = _teleported
|
||||||
|
|
||||||
private val _locationNames = MutableLiveData<Map<GeohashChannelLevel, String>>(emptyMap())
|
private val _locationNames = MutableStateFlow<Map<GeohashChannelLevel, String>>(emptyMap())
|
||||||
val locationNames: LiveData<Map<GeohashChannelLevel, String>> = _locationNames
|
val locationNames: StateFlow<Map<GeohashChannelLevel, String>> = _locationNames
|
||||||
|
|
||||||
// Add a new LiveData property to indicate when location is being fetched
|
private val _isLoadingLocation = MutableStateFlow(false)
|
||||||
private val _isLoadingLocation = MutableLiveData(false)
|
val isLoadingLocation: StateFlow<Boolean> = _isLoadingLocation
|
||||||
val isLoadingLocation: LiveData<Boolean> = _isLoadingLocation
|
|
||||||
|
|
||||||
// Add a new LiveData property to track if location services are enabled by user
|
private val _locationServicesEnabled = MutableStateFlow(false)
|
||||||
private val _locationServicesEnabled = MutableLiveData(false)
|
val locationServicesEnabled: StateFlow<Boolean> = _locationServicesEnabled
|
||||||
val locationServicesEnabled: LiveData<Boolean> = _locationServicesEnabled
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updatePermissionState()
|
updatePermissionState()
|
||||||
@@ -102,15 +100,15 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
when (getCurrentPermissionStatus()) {
|
when (getCurrentPermissionStatus()) {
|
||||||
PermissionState.NOT_DETERMINED -> {
|
PermissionState.NOT_DETERMINED -> {
|
||||||
Log.d(TAG, "Permission not determined - user needs to grant in app settings")
|
Log.d(TAG, "Permission not determined - user needs to grant in app settings")
|
||||||
_permissionState.postValue(PermissionState.NOT_DETERMINED)
|
_permissionState.value = PermissionState.NOT_DETERMINED
|
||||||
}
|
}
|
||||||
PermissionState.DENIED, PermissionState.RESTRICTED -> {
|
PermissionState.DENIED, PermissionState.RESTRICTED -> {
|
||||||
Log.d(TAG, "Permission denied or restricted")
|
Log.d(TAG, "Permission denied or restricted")
|
||||||
_permissionState.postValue(PermissionState.DENIED)
|
_permissionState.value = PermissionState.DENIED
|
||||||
}
|
}
|
||||||
PermissionState.AUTHORIZED -> {
|
PermissionState.AUTHORIZED -> {
|
||||||
Log.d(TAG, "Permission authorized - requesting location")
|
Log.d(TAG, "Permission authorized - requesting location")
|
||||||
_permissionState.postValue(PermissionState.AUTHORIZED)
|
_permissionState.value = PermissionState.AUTHORIZED
|
||||||
requestOneShotLocation()
|
requestOneShotLocation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +178,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
lastLocation?.let { location ->
|
lastLocation?.let { location ->
|
||||||
when (channel) {
|
when (channel) {
|
||||||
is ChannelID.Mesh -> {
|
is ChannelID.Mesh -> {
|
||||||
_teleported.postValue(false)
|
_teleported.value = false
|
||||||
}
|
}
|
||||||
is ChannelID.Location -> {
|
is ChannelID.Location -> {
|
||||||
val currentGeohash = Geohash.encode(
|
val currentGeohash = Geohash.encode(
|
||||||
@@ -189,7 +187,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
precision = channel.channel.level.precision
|
precision = channel.channel.level.precision
|
||||||
)
|
)
|
||||||
val isTeleportedNow = currentGeohash != channel.channel.geohash
|
val isTeleportedNow = currentGeohash != channel.channel.geohash
|
||||||
_teleported.postValue(isTeleportedNow)
|
_teleported.value = isTeleportedNow
|
||||||
Log.d(TAG, "Teleported (immediate recompute): $isTeleportedNow (current: $currentGeohash, selected: ${channel.channel.geohash})")
|
Log.d(TAG, "Teleported (immediate recompute): $isTeleportedNow (current: $currentGeohash, selected: ${channel.channel.geohash})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +199,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun setTeleported(teleported: Boolean) {
|
fun setTeleported(teleported: Boolean) {
|
||||||
Log.d(TAG, "Setting teleported status: $teleported")
|
Log.d(TAG, "Setting teleported status: $teleported")
|
||||||
_teleported.postValue(teleported)
|
_teleported.value = teleported
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -209,7 +207,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun enableLocationServices() {
|
fun enableLocationServices() {
|
||||||
Log.d(TAG, "enableLocationServices() called by user")
|
Log.d(TAG, "enableLocationServices() called by user")
|
||||||
_locationServicesEnabled.postValue(true)
|
_locationServicesEnabled.value = true
|
||||||
saveLocationServicesState(true)
|
saveLocationServicesState(true)
|
||||||
|
|
||||||
// If we have permission, start location operations
|
// If we have permission, start location operations
|
||||||
@@ -223,15 +221,15 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun disableLocationServices() {
|
fun disableLocationServices() {
|
||||||
Log.d(TAG, "disableLocationServices() called by user")
|
Log.d(TAG, "disableLocationServices() called by user")
|
||||||
_locationServicesEnabled.postValue(false)
|
_locationServicesEnabled.value = false
|
||||||
saveLocationServicesState(false)
|
saveLocationServicesState(false)
|
||||||
|
|
||||||
// Stop any ongoing location operations
|
// Stop any ongoing location operations
|
||||||
endLiveRefresh()
|
endLiveRefresh()
|
||||||
|
|
||||||
// Clear available channels when location is disabled
|
// Clear available channels when location is disabled
|
||||||
_availableChannels.postValue(emptyList())
|
_availableChannels.value = emptyList()
|
||||||
_locationNames.postValue(emptyMap())
|
_locationNames.value = emptyMap()
|
||||||
|
|
||||||
// If user had a location channel selected, switch back to mesh
|
// If user had a location channel selected, switch back to mesh
|
||||||
if (_selectedChannel.value is ChannelID.Location) {
|
if (_selectedChannel.value is ChannelID.Location) {
|
||||||
@@ -243,7 +241,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
* Check if location services are enabled by the user
|
* Check if location services are enabled by the user
|
||||||
*/
|
*/
|
||||||
fun isLocationServicesEnabled(): Boolean {
|
fun isLocationServicesEnabled(): Boolean {
|
||||||
return _locationServicesEnabled.value ?: false
|
return _locationServicesEnabled.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Location Operations
|
// MARK: - Location Operations
|
||||||
@@ -280,13 +278,13 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
if (lastKnownLocation != null) {
|
if (lastKnownLocation != null) {
|
||||||
Log.d(TAG, "Using last known location: ${lastKnownLocation.latitude}, ${lastKnownLocation.longitude}")
|
Log.d(TAG, "Using last known location: ${lastKnownLocation.latitude}, ${lastKnownLocation.longitude}")
|
||||||
lastLocation = lastKnownLocation
|
lastLocation = lastKnownLocation
|
||||||
_isLoadingLocation.postValue(false) // Make sure loading state is off
|
_isLoadingLocation.value = false // Make sure loading state is off
|
||||||
computeChannels(lastKnownLocation)
|
computeChannels(lastKnownLocation)
|
||||||
reverseGeocodeIfNeeded(lastKnownLocation)
|
reverseGeocodeIfNeeded(lastKnownLocation)
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "No last known location available")
|
Log.d(TAG, "No last known location available")
|
||||||
// Set loading state to true so UI can show a spinner
|
// Set loading state to true so UI can show a spinner
|
||||||
_isLoadingLocation.postValue(true)
|
_isLoadingLocation.value = true
|
||||||
|
|
||||||
// Request a fresh location only when we don't have a last known location
|
// Request a fresh location only when we don't have a last known location
|
||||||
Log.d(TAG, "Requesting fresh location...")
|
Log.d(TAG, "Requesting fresh location...")
|
||||||
@@ -294,7 +292,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "Security exception requesting location: ${e.message}")
|
Log.e(TAG, "Security exception requesting location: ${e.message}")
|
||||||
_isLoadingLocation.postValue(false) // Turn off loading state on error
|
_isLoadingLocation.value = false // Turn off loading state on error
|
||||||
updatePermissionState()
|
updatePermissionState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,7 +306,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
reverseGeocodeIfNeeded(location)
|
reverseGeocodeIfNeeded(location)
|
||||||
|
|
||||||
// Update loading state to indicate we have a location now
|
// Update loading state to indicate we have a location now
|
||||||
_isLoadingLocation.postValue(false)
|
_isLoadingLocation.value = false
|
||||||
|
|
||||||
// Remove this listener after getting the update
|
// Remove this listener after getting the update
|
||||||
try {
|
try {
|
||||||
@@ -322,13 +320,13 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
// Request a fresh location update using getCurrentLocation instead of continuous updates
|
// Request a fresh location update using getCurrentLocation instead of continuous updates
|
||||||
private fun requestFreshLocation() {
|
private fun requestFreshLocation() {
|
||||||
if (!hasLocationPermission()) {
|
if (!hasLocationPermission()) {
|
||||||
_isLoadingLocation.postValue(false) // Turn off loading state if no permission
|
_isLoadingLocation.value = false // Turn off loading state if no permission
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Set loading state to true to indicate we're actively trying to get a location
|
// Set loading state to true to indicate we're actively trying to get a location
|
||||||
_isLoadingLocation.postValue(true)
|
_isLoadingLocation.value = true
|
||||||
|
|
||||||
// Try common providers in order of preference
|
// Try common providers in order of preference
|
||||||
val providers = listOf(
|
val providers = listOf(
|
||||||
@@ -358,7 +356,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
Log.w(TAG, "Received null location from getCurrentLocation")
|
Log.w(TAG, "Received null location from getCurrentLocation")
|
||||||
}
|
}
|
||||||
// Update loading state to indicate we have a location now
|
// Update loading state to indicate we have a location now
|
||||||
_isLoadingLocation.postValue(false)
|
_isLoadingLocation.value = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -378,14 +376,14 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
// If no provider was available, turn off loading state
|
// If no provider was available, turn off loading state
|
||||||
if (!providerFound) {
|
if (!providerFound) {
|
||||||
Log.w(TAG, "No location providers available")
|
Log.w(TAG, "No location providers available")
|
||||||
_isLoadingLocation.postValue(false)
|
_isLoadingLocation.value = false
|
||||||
}
|
}
|
||||||
} catch (e: SecurityException) {
|
} catch (e: SecurityException) {
|
||||||
Log.e(TAG, "Security exception requesting location: ${e.message}")
|
Log.e(TAG, "Security exception requesting location: ${e.message}")
|
||||||
_isLoadingLocation.postValue(false) // Turn off loading state on error
|
_isLoadingLocation.value = false // Turn off loading state on error
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error requesting location: ${e.message}")
|
Log.e(TAG, "Error requesting location: ${e.message}")
|
||||||
_isLoadingLocation.postValue(false) // Turn off loading state on error
|
_isLoadingLocation.value = false // Turn off loading state on error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,7 +406,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private fun updatePermissionState() {
|
private fun updatePermissionState() {
|
||||||
val newState = getCurrentPermissionStatus()
|
val newState = getCurrentPermissionStatus()
|
||||||
Log.d(TAG, "Permission state updated to: $newState")
|
Log.d(TAG, "Permission state updated to: $newState")
|
||||||
_permissionState.postValue(newState)
|
_permissionState.value = newState
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasLocationPermission(): Boolean {
|
private fun hasLocationPermission(): Boolean {
|
||||||
@@ -433,13 +431,13 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
Log.v(TAG, "Generated ${level.displayName}: $geohash")
|
Log.v(TAG, "Generated ${level.displayName}: $geohash")
|
||||||
}
|
}
|
||||||
|
|
||||||
_availableChannels.postValue(result)
|
_availableChannels.value = result
|
||||||
|
|
||||||
// Recompute teleported status based on current location vs selected channel
|
// Recompute teleported status based on current location vs selected channel
|
||||||
val selectedChannelValue = _selectedChannel.value
|
val selectedChannelValue = _selectedChannel.value
|
||||||
when (selectedChannelValue) {
|
when (selectedChannelValue) {
|
||||||
is ChannelID.Mesh -> {
|
is ChannelID.Mesh -> {
|
||||||
_teleported.postValue(false)
|
_teleported.value = false
|
||||||
}
|
}
|
||||||
is ChannelID.Location -> {
|
is ChannelID.Location -> {
|
||||||
val currentGeohash = Geohash.encode(
|
val currentGeohash = Geohash.encode(
|
||||||
@@ -448,12 +446,9 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
precision = selectedChannelValue.channel.level.precision
|
precision = selectedChannelValue.channel.level.precision
|
||||||
)
|
)
|
||||||
val isTeleported = currentGeohash != selectedChannelValue.channel.geohash
|
val isTeleported = currentGeohash != selectedChannelValue.channel.geohash
|
||||||
_teleported.postValue(isTeleported)
|
_teleported.value = isTeleported
|
||||||
Log.d(TAG, "Teleported status: $isTeleported (current: $currentGeohash, selected: ${selectedChannelValue.channel.geohash})")
|
Log.d(TAG, "Teleported status: $isTeleported (current: $currentGeohash, selected: ${selectedChannelValue.channel.geohash})")
|
||||||
}
|
}
|
||||||
null -> {
|
|
||||||
_teleported.postValue(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +477,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
val names = namesByLevel(address)
|
val names = namesByLevel(address)
|
||||||
|
|
||||||
Log.d(TAG, "Reverse geocoding result: $names")
|
Log.d(TAG, "Reverse geocoding result: $names")
|
||||||
_locationNames.postValue(names)
|
_locationNames.value = names
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "No reverse geocoding results")
|
Log.w(TAG, "No reverse geocoding results")
|
||||||
}
|
}
|
||||||
@@ -601,26 +596,26 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (channel != null) {
|
if (channel != null) {
|
||||||
_selectedChannel.postValue(channel)
|
_selectedChannel.value = channel
|
||||||
Log.d(TAG, "Restored persisted channel: ${channel.displayName}")
|
Log.d(TAG, "Restored persisted channel: ${channel.displayName}")
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh")
|
Log.d(TAG, "Could not restore persisted channel, defaulting to Mesh")
|
||||||
_selectedChannel.postValue(ChannelID.Mesh)
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "Invalid channel data format in persistence")
|
Log.w(TAG, "Invalid channel data format in persistence")
|
||||||
_selectedChannel.postValue(ChannelID.Mesh)
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "No persisted channel found, defaulting to Mesh")
|
Log.d(TAG, "No persisted channel found, defaulting to Mesh")
|
||||||
_selectedChannel.postValue(ChannelID.Mesh)
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
} catch (e: JsonSyntaxException) {
|
} catch (e: JsonSyntaxException) {
|
||||||
Log.e(TAG, "Failed to parse persisted channel data: ${e.message}")
|
Log.e(TAG, "Failed to parse persisted channel data: ${e.message}")
|
||||||
_selectedChannel.postValue(ChannelID.Mesh)
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load persisted channel: ${e.message}")
|
Log.e(TAG, "Failed to load persisted channel: ${e.message}")
|
||||||
_selectedChannel.postValue(ChannelID.Mesh)
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,7 +624,7 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun clearPersistedChannel() {
|
fun clearPersistedChannel() {
|
||||||
dataManager?.clearLastGeohashChannel()
|
dataManager?.clearLastGeohashChannel()
|
||||||
_selectedChannel.postValue(ChannelID.Mesh)
|
_selectedChannel.value = ChannelID.Mesh
|
||||||
Log.d(TAG, "Cleared persisted channel selection")
|
Log.d(TAG, "Cleared persisted channel selection")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,11 +648,11 @@ class LocationChannelManager private constructor(private val context: Context) {
|
|||||||
private fun loadLocationServicesState() {
|
private fun loadLocationServicesState() {
|
||||||
try {
|
try {
|
||||||
val enabled = dataManager?.isLocationServicesEnabled() ?: false
|
val enabled = dataManager?.isLocationServicesEnabled() ?: false
|
||||||
_locationServicesEnabled.postValue(enabled)
|
_locationServicesEnabled.value = enabled
|
||||||
Log.d(TAG, "Loaded location services state: $enabled")
|
Log.d(TAG, "Loaded location services state: $enabled")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load location services state: ${e.message}")
|
Log.e(TAG, "Failed to load location services state: ${e.message}")
|
||||||
_locationServicesEnabled.postValue(false)
|
_locationServicesEnabled.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.bitchat.android.nostr
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import com.bitchat.android.ui.ChatState
|
import com.bitchat.android.ui.ChatState
|
||||||
import com.bitchat.android.ui.GeoPerson
|
import com.bitchat.android.ui.GeoPerson
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -112,7 +111,7 @@ class GeohashRepository(
|
|||||||
val geohash = currentGeohash
|
val geohash = currentGeohash
|
||||||
if (geohash == null) {
|
if (geohash == null) {
|
||||||
// Use postValue for thread safety - this can be called from background threads
|
// Use postValue for thread safety - this can be called from background threads
|
||||||
state.postGeohashPeople(emptyList())
|
state.setGeohashPeople(emptyList())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val cutoff = Date(System.currentTimeMillis() - 5 * 60 * 1000)
|
val cutoff = Date(System.currentTimeMillis() - 5 * 60 * 1000)
|
||||||
@@ -143,7 +142,7 @@ class GeohashRepository(
|
|||||||
)
|
)
|
||||||
}.sortedByDescending { it.lastSeen }
|
}.sortedByDescending { it.lastSeen }
|
||||||
// Use postValue for thread safety - this can be called from background threads
|
// Use postValue for thread safety - this can be called from background threads
|
||||||
state.postGeohashPeople(people)
|
state.setGeohashPeople(people)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateReactiveParticipantCounts() {
|
fun updateReactiveParticipantCounts() {
|
||||||
@@ -155,7 +154,7 @@ class GeohashRepository(
|
|||||||
counts[gh] = active
|
counts[gh] = active
|
||||||
}
|
}
|
||||||
// Use postValue for thread safety - this can be called from background threads
|
// Use postValue for thread safety - this can be called from background threads
|
||||||
state.postGeohashParticipantCounts(counts)
|
state.setGeohashParticipantCounts(counts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putNostrKeyMapping(tempKeyOrPeer: String, pubkeyHex: String) {
|
fun putNostrKeyMapping(tempKeyOrPeer: String, pubkeyHex: String) {
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ package com.bitchat.android.nostr
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages location notes (kind=1 text notes with geohash tags)
|
* Manages location notes (kind=1 text notes with geohash tags)
|
||||||
* iOS-compatible implementation with LiveData for Android UI binding
|
* iOS-compatible implementation with StateFlow for Android UI binding
|
||||||
*/
|
*/
|
||||||
@MainThread
|
@MainThread
|
||||||
class LocationNotesManager private constructor() {
|
class LocationNotesManager private constructor() {
|
||||||
@@ -63,21 +64,21 @@ class LocationNotesManager private constructor() {
|
|||||||
NO_RELAYS
|
NO_RELAYS
|
||||||
}
|
}
|
||||||
|
|
||||||
// Published state (LiveData for Android)
|
// Published state (StateFlow for Android)
|
||||||
private val _notes = MutableLiveData<List<Note>>(emptyList())
|
private val _notes = MutableStateFlow<List<Note>>(emptyList())
|
||||||
val notes: LiveData<List<Note>> = _notes
|
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
|
||||||
|
|
||||||
private val _geohash = MutableLiveData<String?>(null)
|
private val _geohash = MutableStateFlow<String?>(null)
|
||||||
val geohash: LiveData<String?> = _geohash
|
val geohash: StateFlow<String?> = _geohash.asStateFlow()
|
||||||
|
|
||||||
private val _initialLoadComplete = MutableLiveData(false)
|
private val _initialLoadComplete = MutableStateFlow(false)
|
||||||
val initialLoadComplete: LiveData<Boolean> = _initialLoadComplete
|
val initialLoadComplete: StateFlow<Boolean> = _initialLoadComplete.asStateFlow()
|
||||||
|
|
||||||
private val _state = MutableLiveData(State.IDLE)
|
private val _state = MutableStateFlow(State.IDLE)
|
||||||
val state: LiveData<State> = _state
|
val state: StateFlow<State> = _state.asStateFlow()
|
||||||
|
|
||||||
private val _errorMessage = MutableLiveData<String?>(null)
|
private val _errorMessage = MutableStateFlow<String?>(null)
|
||||||
val errorMessage: LiveData<String?> = _errorMessage
|
val errorMessage: StateFlow<String?> = _errorMessage.asStateFlow()
|
||||||
|
|
||||||
// Private state
|
// Private state
|
||||||
private var subscriptionIDs: MutableMap<String, String> = mutableMapOf()
|
private var subscriptionIDs: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package com.bitchat.android.nostr
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* High-level Nostr client that manages identity, connections, and messaging
|
* High-level Nostr client that manages identity, connections, and messaging
|
||||||
@@ -30,11 +31,11 @@ class NostrClient private constructor(private val context: Context) {
|
|||||||
private var currentIdentity: NostrIdentity? = null
|
private var currentIdentity: NostrIdentity? = null
|
||||||
|
|
||||||
// Client state
|
// Client state
|
||||||
private val _isInitialized = MutableLiveData<Boolean>()
|
private val _isInitialized = MutableStateFlow(false)
|
||||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
||||||
|
|
||||||
private val _currentNpub = MutableLiveData<String>()
|
private val _currentNpub = MutableStateFlow<String?>(null)
|
||||||
val currentNpub: LiveData<String> = _currentNpub
|
val currentNpub: StateFlow<String?> = _currentNpub.asStateFlow()
|
||||||
|
|
||||||
// Message processing
|
// Message processing
|
||||||
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
@@ -53,21 +54,21 @@ class NostrClient private constructor(private val context: Context) {
|
|||||||
currentIdentity = NostrIdentityBridge.getCurrentNostrIdentity(context)
|
currentIdentity = NostrIdentityBridge.getCurrentNostrIdentity(context)
|
||||||
|
|
||||||
if (currentIdentity != null) {
|
if (currentIdentity != null) {
|
||||||
_currentNpub.postValue(currentIdentity!!.npub)
|
_currentNpub.value = currentIdentity!!.npub
|
||||||
Log.i(TAG, "✅ Nostr identity loaded: ${currentIdentity!!.getShortNpub()}")
|
Log.i(TAG, "✅ Nostr identity loaded: ${currentIdentity!!.getShortNpub()}")
|
||||||
|
|
||||||
// Connect to relays
|
// Connect to relays
|
||||||
relayManager.connect()
|
relayManager.connect()
|
||||||
|
|
||||||
_isInitialized.postValue(true)
|
_isInitialized.value = true
|
||||||
Log.i(TAG, "✅ Nostr client initialized successfully")
|
Log.i(TAG, "✅ Nostr client initialized successfully")
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "❌ Failed to load/create Nostr identity")
|
Log.e(TAG, "❌ Failed to load/create Nostr identity")
|
||||||
_isInitialized.postValue(false)
|
_isInitialized.value = false
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to initialize Nostr client: ${e.message}")
|
Log.e(TAG, "❌ Failed to initialize Nostr client: ${e.message}")
|
||||||
_isInitialized.postValue(false)
|
_isInitialized.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ class NostrClient private constructor(private val context: Context) {
|
|||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
Log.d(TAG, "Shutting down Nostr client")
|
Log.d(TAG, "Shutting down Nostr client")
|
||||||
relayManager.disconnect()
|
relayManager.disconnect()
|
||||||
_isInitialized.postValue(false)
|
_isInitialized.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,12 +228,12 @@ class NostrClient private constructor(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Get relay connection status
|
* Get relay connection status
|
||||||
*/
|
*/
|
||||||
val relayConnectionStatus: LiveData<Boolean> = relayManager.isConnected
|
val relayConnectionStatus: StateFlow<Boolean> = relayManager.isConnected
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get relay information
|
* Get relay information
|
||||||
*/
|
*/
|
||||||
val relayInfo: LiveData<List<NostrRelayManager.Relay>> = relayManager.relays
|
val relayInfo: StateFlow<List<NostrRelayManager.Relay>> = relayManager.relays
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package com.bitchat.android.nostr
|
package com.bitchat.android.nostr
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
@@ -72,11 +73,11 @@ class NostrRelayManager private constructor() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Published state
|
// Published state
|
||||||
private val _relays = MutableLiveData<List<Relay>>()
|
private val _relays = MutableStateFlow<List<Relay>>(emptyList())
|
||||||
val relays: LiveData<List<Relay>> = _relays
|
val relays: StateFlow<List<Relay>> = _relays.asStateFlow()
|
||||||
|
|
||||||
private val _isConnected = MutableLiveData<Boolean>()
|
private val _isConnected = MutableStateFlow<Boolean>(false)
|
||||||
val isConnected: LiveData<Boolean> = _isConnected
|
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
|
||||||
|
|
||||||
// Internal state
|
// Internal state
|
||||||
private val relaysList = mutableListOf<Relay>()
|
private val relaysList = mutableListOf<Relay>()
|
||||||
@@ -226,14 +227,14 @@ class NostrRelayManager private constructor() {
|
|||||||
"wss://nostr21.com"
|
"wss://nostr21.com"
|
||||||
)
|
)
|
||||||
relaysList.addAll(defaultRelayUrls.map { Relay(it) })
|
relaysList.addAll(defaultRelayUrls.map { Relay(it) })
|
||||||
_relays.postValue(relaysList.toList())
|
_relays.value = relaysList.toList()
|
||||||
updateConnectionStatus()
|
updateConnectionStatus()
|
||||||
Log.d(TAG, "✅ NostrRelayManager initialized with ${relaysList.size} default relays")
|
Log.d(TAG, "✅ NostrRelayManager initialized with ${relaysList.size} default relays")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize NostrRelayManager: ${e.message}", e)
|
Log.e(TAG, "Failed to initialize NostrRelayManager: ${e.message}", e)
|
||||||
// Initialize with empty list as fallback
|
// Initialize with empty list as fallback
|
||||||
_relays.postValue(emptyList())
|
_relays.value = emptyList()
|
||||||
_isConnected.postValue(false)
|
_isConnected.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,12 +798,12 @@ class NostrRelayManager private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRelaysList() {
|
private fun updateRelaysList() {
|
||||||
_relays.postValue(relaysList.toList())
|
_relays.value = relaysList.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateConnectionStatus() {
|
private fun updateConnectionStatus() {
|
||||||
val connected = relaysList.any { it.isConnected }
|
val connected = relaysList.any { it.isConnected }
|
||||||
_isConnected.postValue(connected)
|
_isConnected.value = connected
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateSubscriptionId(): String {
|
private fun generateSubscriptionId(): String {
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ import androidx.compose.ui.unit.sp
|
|||||||
import com.bitchat.android.nostr.NostrProofOfWork
|
import com.bitchat.android.nostr.NostrProofOfWork
|
||||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.R
|
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.TorMode
|
||||||
import com.bitchat.android.net.TorPreferenceManager
|
import com.bitchat.android.net.TorPreferenceManager
|
||||||
import com.bitchat.android.net.ArtiTorManager
|
import com.bitchat.android.net.ArtiTorManager
|
||||||
@@ -243,7 +245,7 @@ fun AboutSheet(
|
|||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
.padding(top = 24.dp, bottom = 8.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(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -279,8 +281,8 @@ fun AboutSheet(
|
|||||||
PoWPreferenceManager.init(context)
|
PoWPreferenceManager.init(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
val powEnabled by PoWPreferenceManager.powEnabled.collectAsState()
|
val powEnabled by PoWPreferenceManager.powEnabled.collectAsStateWithLifecycle()
|
||||||
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState()
|
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
@@ -595,23 +597,17 @@ fun AboutSheet(
|
|||||||
.height(64.dp)
|
.height(64.dp)
|
||||||
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
|
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
|
||||||
) {
|
) {
|
||||||
TextButton(
|
CloseButton(
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.close_plain),
|
|
||||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
|
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Password prompt dialog for password-protected channels
|
* Password prompt dialog for password-protected channels
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.compose.material.icons.filled.*
|
|||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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 com.bitchat.android.core.ui.utils.singleOrTripleClickable
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header components for ChatScreen
|
* Header components for ChatScreen
|
||||||
@@ -248,10 +248,10 @@ fun ChatHeaderContent(
|
|||||||
when {
|
when {
|
||||||
selectedPrivatePeer != null -> {
|
selectedPrivatePeer != null -> {
|
||||||
// Private chat header - Fully reactive state tracking
|
// Private chat header - Fully reactive state tracking
|
||||||
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
||||||
val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
|
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||||
val peerSessionStates by viewModel.peerSessionStates.observeAsState(emptyMap())
|
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
|
||||||
val peerNicknames by viewModel.peerNicknames.observeAsState(emptyMap())
|
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Reactive favorite computation - no more static lookups!
|
// Reactive favorite computation - no more static lookups!
|
||||||
val isFavorite = isFavoriteReactive(
|
val isFavorite = isFavoriteReactive(
|
||||||
@@ -264,8 +264,8 @@ fun ChatHeaderContent(
|
|||||||
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, isFav=$isFavorite, sessionState=$sessionState")
|
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, isFav=$isFavorite, sessionState=$sessionState")
|
||||||
|
|
||||||
// Pass geohash context and people for NIP-17 chat title formatting
|
// Pass geohash context and people for NIP-17 chat title formatting
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
val geohashPeople by viewModel.geohashPeople.observeAsState(emptyList())
|
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
PrivateChatHeader(
|
PrivateChatHeader(
|
||||||
peerID = selectedPrivatePeer,
|
peerID = selectedPrivatePeer,
|
||||||
@@ -523,18 +523,18 @@ private fun MainHeader(
|
|||||||
viewModel: ChatViewModel
|
viewModel: ChatViewModel
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||||
val joinedChannels by viewModel.joinedChannels.observeAsState(emptySet())
|
val joinedChannels by viewModel.joinedChannels.collectAsStateWithLifecycle()
|
||||||
val hasUnreadChannels by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
val hasUnreadChannels by viewModel.unreadChannelMessages.collectAsStateWithLifecycle()
|
||||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||||
val isConnected by viewModel.isConnected.observeAsState(false)
|
val isConnected by viewModel.isConnected.collectAsStateWithLifecycle()
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
val geohashPeople by viewModel.geohashPeople.observeAsState(emptyList())
|
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Bookmarks store for current geohash toggle (iOS parity)
|
// Bookmarks store for current geohash toggle (iOS parity)
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
val bookmarksStore = remember { com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(context) }
|
val bookmarksStore = remember { com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(context) }
|
||||||
val bookmarks by bookmarksStore.bookmarks.observeAsState(emptyList())
|
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -653,8 +653,8 @@ private fun LocationChannelsButton(
|
|||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
// Get current channel selection from location manager
|
// Get current channel selection from location manager
|
||||||
val selectedChannel by viewModel.selectedLocationChannel.observeAsState()
|
val selectedChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
val teleported by viewModel.isTeleported.observeAsState(false)
|
val teleported by viewModel.isTeleported.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val (badgeText, badgeColor) = when (selectedChannel) {
|
val (badgeText, badgeColor) = when (selectedChannel) {
|
||||||
is com.bitchat.android.geohash.ChannelID.Mesh -> {
|
is com.bitchat.android.geohash.ChannelID.Mesh -> {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.*
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.Alignment
|
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.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.ui.media.FullScreenImageViewer
|
import com.bitchat.android.ui.media.FullScreenImageViewer
|
||||||
|
|
||||||
@@ -41,22 +41,22 @@ import com.bitchat.android.ui.media.FullScreenImageViewer
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(viewModel: ChatViewModel) {
|
fun ChatScreen(viewModel: ChatViewModel) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val messages by viewModel.messages.observeAsState(emptyList())
|
val messages by viewModel.messages.collectAsStateWithLifecycle()
|
||||||
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||||
val nickname by viewModel.nickname.observeAsState("")
|
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||||
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.observeAsState()
|
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
|
||||||
val currentChannel by viewModel.currentChannel.observeAsState()
|
val currentChannel by viewModel.currentChannel.collectAsStateWithLifecycle()
|
||||||
val joinedChannels by viewModel.joinedChannels.observeAsState(emptySet())
|
val joinedChannels by viewModel.joinedChannels.collectAsStateWithLifecycle()
|
||||||
val hasUnreadChannels by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
val hasUnreadChannels by viewModel.unreadChannelMessages.collectAsStateWithLifecycle()
|
||||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||||
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
||||||
val channelMessages by viewModel.channelMessages.observeAsState(emptyMap())
|
val channelMessages by viewModel.channelMessages.collectAsStateWithLifecycle()
|
||||||
val showSidebar by viewModel.showSidebar.observeAsState(false)
|
val showSidebar by viewModel.showSidebar.collectAsStateWithLifecycle()
|
||||||
val showCommandSuggestions by viewModel.showCommandSuggestions.observeAsState(false)
|
val showCommandSuggestions by viewModel.showCommandSuggestions.collectAsStateWithLifecycle()
|
||||||
val commandSuggestions by viewModel.commandSuggestions.observeAsState(emptyList())
|
val commandSuggestions by viewModel.commandSuggestions.collectAsStateWithLifecycle()
|
||||||
val showMentionSuggestions by viewModel.showMentionSuggestions.observeAsState(false)
|
val showMentionSuggestions by viewModel.showMentionSuggestions.collectAsStateWithLifecycle()
|
||||||
val mentionSuggestions by viewModel.mentionSuggestions.observeAsState(emptyList())
|
val mentionSuggestions by viewModel.mentionSuggestions.collectAsStateWithLifecycle()
|
||||||
val showAppInfo by viewModel.showAppInfo.observeAsState(false)
|
val showAppInfo by viewModel.showAppInfo.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
var messageText by remember { mutableStateOf(TextFieldValue("")) }
|
var messageText by remember { mutableStateOf(TextFieldValue("")) }
|
||||||
var showPasswordPrompt by remember { mutableStateOf(false) }
|
var showPasswordPrompt by remember { mutableStateOf(false) }
|
||||||
@@ -78,11 +78,11 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
showPasswordDialog = showPasswordPrompt
|
showPasswordDialog = showPasswordPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
val isConnected by viewModel.isConnected.observeAsState(false)
|
val isConnected by viewModel.isConnected.collectAsStateWithLifecycle()
|
||||||
val passwordPromptChannel by viewModel.passwordPromptChannel.observeAsState(null)
|
val passwordPromptChannel by viewModel.passwordPromptChannel.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Get location channel info for timeline switching
|
// 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)
|
// Determine what messages to show based on current context (unified timelines)
|
||||||
val displayMessages = when {
|
val displayMessages = when {
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import com.bitchat.android.model.BitchatMessage
|
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
|
* 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
|
* Contains all the observable state for the chat system
|
||||||
*/
|
*/
|
||||||
class ChatState {
|
class ChatState(
|
||||||
|
scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
|
||||||
// Core messages and peer state
|
// Core messages and peer state
|
||||||
private val _messages = MutableLiveData<List<BitchatMessage>>(emptyList())
|
private val _messages = MutableStateFlow<List<BitchatMessage>>(emptyList())
|
||||||
val messages: LiveData<List<BitchatMessage>> = _messages
|
val messages: StateFlow<List<BitchatMessage>> = _messages.asStateFlow()
|
||||||
|
|
||||||
private val _connectedPeers = MutableLiveData<List<String>>(emptyList())
|
private val _connectedPeers = MutableStateFlow<List<String>>(emptyList())
|
||||||
val connectedPeers: LiveData<List<String>> = _connectedPeers
|
val connectedPeers: StateFlow<List<String>> = _connectedPeers.asStateFlow()
|
||||||
|
|
||||||
private val _nickname = MutableLiveData<String>()
|
private val _nickname = MutableStateFlow<String>("")
|
||||||
val nickname: LiveData<String> = _nickname
|
val nickname: StateFlow<String> = _nickname.asStateFlow()
|
||||||
|
|
||||||
private val _isConnected = MutableLiveData<Boolean>(false)
|
private val _isConnected = MutableStateFlow<Boolean>(false)
|
||||||
val isConnected: LiveData<Boolean> = _isConnected
|
val isConnected: StateFlow<Boolean> = _isConnected.asStateFlow()
|
||||||
|
|
||||||
// Private chats
|
// Private chats
|
||||||
private val _privateChats = MutableLiveData<Map<String, List<BitchatMessage>>>(emptyMap())
|
private val _privateChats = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
|
||||||
val privateChats: LiveData<Map<String, List<BitchatMessage>>> = _privateChats
|
val privateChats: StateFlow<Map<String, List<BitchatMessage>>> = _privateChats.asStateFlow()
|
||||||
|
|
||||||
private val _selectedPrivateChatPeer = MutableLiveData<String?>(null)
|
private val _selectedPrivateChatPeer = MutableStateFlow<String?>(null)
|
||||||
val selectedPrivateChatPeer: LiveData<String?> = _selectedPrivateChatPeer
|
val selectedPrivateChatPeer: StateFlow<String?> = _selectedPrivateChatPeer.asStateFlow()
|
||||||
|
|
||||||
private val _unreadPrivateMessages = MutableLiveData<Set<String>>(emptySet())
|
private val _unreadPrivateMessages = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val unreadPrivateMessages: LiveData<Set<String>> = _unreadPrivateMessages
|
val unreadPrivateMessages: StateFlow<Set<String>> = _unreadPrivateMessages.asStateFlow()
|
||||||
|
|
||||||
// Channels
|
// Channels
|
||||||
private val _joinedChannels = MutableLiveData<Set<String>>(emptySet())
|
private val _joinedChannels = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val joinedChannels: LiveData<Set<String>> = _joinedChannels
|
val joinedChannels: StateFlow<Set<String>> = _joinedChannels.asStateFlow()
|
||||||
|
|
||||||
private val _currentChannel = MutableLiveData<String?>(null)
|
private val _currentChannel = MutableStateFlow<String?>(null)
|
||||||
val currentChannel: LiveData<String?> = _currentChannel
|
val currentChannel: StateFlow<String?> = _currentChannel.asStateFlow()
|
||||||
|
|
||||||
private val _channelMessages = MutableLiveData<Map<String, List<BitchatMessage>>>(emptyMap())
|
private val _channelMessages = MutableStateFlow<Map<String, List<BitchatMessage>>>(emptyMap())
|
||||||
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = _channelMessages
|
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = _channelMessages.asStateFlow()
|
||||||
|
|
||||||
private val _unreadChannelMessages = MutableLiveData<Map<String, Int>>(emptyMap())
|
private val _unreadChannelMessages = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
val unreadChannelMessages: LiveData<Map<String, Int>> = _unreadChannelMessages
|
val unreadChannelMessages: StateFlow<Map<String, Int>> = _unreadChannelMessages.asStateFlow()
|
||||||
|
|
||||||
private val _passwordProtectedChannels = MutableLiveData<Set<String>>(emptySet())
|
private val _passwordProtectedChannels = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val passwordProtectedChannels: LiveData<Set<String>> = _passwordProtectedChannels
|
val passwordProtectedChannels: StateFlow<Set<String>> = _passwordProtectedChannels.asStateFlow()
|
||||||
|
|
||||||
private val _showPasswordPrompt = MutableLiveData<Boolean>(false)
|
private val _showPasswordPrompt = MutableStateFlow<Boolean>(false)
|
||||||
val showPasswordPrompt: LiveData<Boolean> = _showPasswordPrompt
|
val showPasswordPrompt: StateFlow<Boolean> = _showPasswordPrompt.asStateFlow()
|
||||||
|
|
||||||
private val _passwordPromptChannel = MutableLiveData<String?>(null)
|
private val _passwordPromptChannel = MutableStateFlow<String?>(null)
|
||||||
val passwordPromptChannel: LiveData<String?> = _passwordPromptChannel
|
val passwordPromptChannel: StateFlow<String?> = _passwordPromptChannel.asStateFlow()
|
||||||
|
|
||||||
// Sidebar state
|
// Sidebar state
|
||||||
private val _showSidebar = MutableLiveData(false)
|
private val _showSidebar = MutableStateFlow(false)
|
||||||
val showSidebar: LiveData<Boolean> = _showSidebar
|
val showSidebar: StateFlow<Boolean> = _showSidebar.asStateFlow()
|
||||||
|
|
||||||
// Command autocomplete
|
// Command autocomplete
|
||||||
private val _showCommandSuggestions = MutableLiveData(false)
|
private val _showCommandSuggestions = MutableStateFlow(false)
|
||||||
val showCommandSuggestions: LiveData<Boolean> = _showCommandSuggestions
|
val showCommandSuggestions: StateFlow<Boolean> = _showCommandSuggestions.asStateFlow()
|
||||||
|
|
||||||
private val _commandSuggestions = MutableLiveData<List<CommandSuggestion>>(emptyList())
|
private val _commandSuggestions = MutableStateFlow<List<CommandSuggestion>>(emptyList())
|
||||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = _commandSuggestions
|
val commandSuggestions: StateFlow<List<CommandSuggestion>> = _commandSuggestions.asStateFlow()
|
||||||
|
|
||||||
// Mention autocomplete
|
// Mention autocomplete
|
||||||
private val _showMentionSuggestions = MutableLiveData(false)
|
private val _showMentionSuggestions = MutableStateFlow(false)
|
||||||
val showMentionSuggestions: LiveData<Boolean> = _showMentionSuggestions
|
val showMentionSuggestions: StateFlow<Boolean> = _showMentionSuggestions.asStateFlow()
|
||||||
|
|
||||||
private val _mentionSuggestions = MutableLiveData<List<String>>(emptyList())
|
private val _mentionSuggestions = MutableStateFlow<List<String>>(emptyList())
|
||||||
val mentionSuggestions: LiveData<List<String>> = _mentionSuggestions
|
val mentionSuggestions: StateFlow<List<String>> = _mentionSuggestions.asStateFlow()
|
||||||
|
|
||||||
// Favorites
|
// Favorites
|
||||||
private val _favoritePeers = MutableLiveData<Set<String>>(emptySet())
|
private val _favoritePeers = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val favoritePeers: LiveData<Set<String>> = _favoritePeers
|
val favoritePeers: StateFlow<Set<String>> = _favoritePeers.asStateFlow()
|
||||||
|
|
||||||
// Noise session states for peers (for reactive UI updates)
|
// Noise session states for peers (for reactive UI updates)
|
||||||
private val _peerSessionStates = MutableLiveData<Map<String, String>>(emptyMap())
|
private val _peerSessionStates = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val peerSessionStates: LiveData<Map<String, String>> = _peerSessionStates
|
val peerSessionStates: StateFlow<Map<String, String>> = _peerSessionStates.asStateFlow()
|
||||||
|
|
||||||
// Peer fingerprint state for reactive favorites (for reactive UI updates)
|
// Peer fingerprint state for reactive favorites (for reactive UI updates)
|
||||||
private val _peerFingerprints = MutableLiveData<Map<String, String>>(emptyMap())
|
private val _peerFingerprints = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val peerFingerprints: LiveData<Map<String, String>> = _peerFingerprints
|
val peerFingerprints: StateFlow<Map<String, String>> = _peerFingerprints.asStateFlow()
|
||||||
|
|
||||||
private val _peerNicknames = MutableLiveData<Map<String, String>>(emptyMap())
|
private val _peerNicknames = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
val peerNicknames: LiveData<Map<String, String>> = _peerNicknames
|
val peerNicknames: StateFlow<Map<String, String>> = _peerNicknames.asStateFlow()
|
||||||
|
|
||||||
private val _peerRSSI = MutableLiveData<Map<String, Int>>(emptyMap())
|
private val _peerRSSI = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
val peerRSSI: LiveData<Map<String, Int>> = _peerRSSI
|
val peerRSSI: StateFlow<Map<String, Int>> = _peerRSSI.asStateFlow()
|
||||||
|
|
||||||
// Direct connection status per peer (for live UI updates)
|
// Direct connection status per peer (for live UI updates)
|
||||||
private val _peerDirect = MutableLiveData<Map<String, Boolean>>(emptyMap())
|
private val _peerDirect = MutableStateFlow<Map<String, Boolean>>(emptyMap())
|
||||||
val peerDirect: LiveData<Map<String, Boolean>> = _peerDirect
|
val peerDirect: StateFlow<Map<String, Boolean>> = _peerDirect.asStateFlow()
|
||||||
|
|
||||||
// peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager
|
// peerIDToPublicKeyFingerprint REMOVED - fingerprints now handled centrally in PeerManager
|
||||||
|
|
||||||
// Navigation state
|
// Navigation state
|
||||||
private val _showAppInfo = MutableLiveData<Boolean>(false)
|
private val _showAppInfo = MutableStateFlow<Boolean>(false)
|
||||||
val showAppInfo: LiveData<Boolean> = _showAppInfo
|
val showAppInfo: StateFlow<Boolean> = _showAppInfo.asStateFlow()
|
||||||
|
|
||||||
// Location channels state (for Nostr geohash features)
|
// Location channels state (for Nostr geohash features)
|
||||||
private val _selectedLocationChannel = MutableLiveData<com.bitchat.android.geohash.ChannelID?>(com.bitchat.android.geohash.ChannelID.Mesh)
|
private val _selectedLocationChannel = MutableStateFlow<com.bitchat.android.geohash.ChannelID?>(com.bitchat.android.geohash.ChannelID.Mesh)
|
||||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = _selectedLocationChannel
|
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = _selectedLocationChannel.asStateFlow()
|
||||||
|
|
||||||
private val _isTeleported = MutableLiveData<Boolean>(false)
|
private val _isTeleported = MutableStateFlow<Boolean>(false)
|
||||||
val isTeleported: LiveData<Boolean> = _isTeleported
|
val isTeleported: StateFlow<Boolean> = _isTeleported.asStateFlow()
|
||||||
|
|
||||||
// Geohash people state (iOS-compatible)
|
// Geohash people state (iOS-compatible)
|
||||||
private val _geohashPeople = MutableLiveData<List<GeoPerson>>(emptyList())
|
private val _geohashPeople = MutableStateFlow<List<GeoPerson>>(emptyList())
|
||||||
val geohashPeople: LiveData<List<GeoPerson>> = _geohashPeople
|
val geohashPeople: StateFlow<List<GeoPerson>> = _geohashPeople.asStateFlow()
|
||||||
// For background thread updates by repositories/handlers in their own scopes
|
|
||||||
val geohashPeopleMutable: MutableLiveData<List<GeoPerson>> get() = _geohashPeople
|
|
||||||
|
|
||||||
|
private val _teleportedGeo = MutableStateFlow<Set<String>>(emptySet())
|
||||||
private val _teleportedGeo = MutableLiveData<Set<String>>(emptySet())
|
val teleportedGeo: StateFlow<Set<String>> = _teleportedGeo.asStateFlow()
|
||||||
val teleportedGeo: LiveData<Set<String>> = _teleportedGeo
|
|
||||||
|
|
||||||
// Geohash participant counts reactive state (for real-time location channel counts)
|
// Geohash participant counts reactive state (for real-time location channel counts)
|
||||||
private val _geohashParticipantCounts = MutableLiveData<Map<String, Int>>(emptyMap())
|
private val _geohashParticipantCounts = MutableStateFlow<Map<String, Int>>(emptyMap())
|
||||||
val geohashParticipantCounts: LiveData<Map<String, Int>> = _geohashParticipantCounts
|
val geohashParticipantCounts: StateFlow<Map<String, Int>> = _geohashParticipantCounts.asStateFlow()
|
||||||
|
|
||||||
// Unread state computed properties
|
|
||||||
val hasUnreadChannels: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
|
||||||
val hasUnreadPrivateMessages: MediatorLiveData<Boolean> = MediatorLiveData<Boolean>()
|
|
||||||
|
|
||||||
init {
|
val hasUnreadChannels: StateFlow<Boolean> = _unreadChannelMessages
|
||||||
// Initialize unread state mediators
|
.map { unreadMap -> unreadMap.values.any { it > 0 } }
|
||||||
hasUnreadChannels.addSource(_unreadChannelMessages) { unreadMap ->
|
.stateIn(
|
||||||
hasUnreadChannels.value = unreadMap.values.any { it > 0 }
|
scope = scope,
|
||||||
}
|
started = WhileSubscribed(5_000),
|
||||||
|
initialValue = false
|
||||||
|
)
|
||||||
|
|
||||||
hasUnreadPrivateMessages.addSource(_unreadPrivateMessages) { unreadSet ->
|
val hasUnreadPrivateMessages: StateFlow<Boolean> = _unreadPrivateMessages
|
||||||
hasUnreadPrivateMessages.value = unreadSet.isNotEmpty()
|
.map { unreadSet -> unreadSet.isNotEmpty() }
|
||||||
}
|
.stateIn(
|
||||||
}
|
scope = scope,
|
||||||
|
started = WhileSubscribed(5_000),
|
||||||
|
initialValue = false
|
||||||
|
)
|
||||||
|
|
||||||
// Getters for internal state access
|
// Getters for internal state access
|
||||||
fun getMessagesValue() = _messages.value ?: emptyList()
|
fun getMessagesValue() = _messages.value
|
||||||
fun getConnectedPeersValue() = _connectedPeers.value ?: emptyList()
|
fun getConnectedPeersValue() = _connectedPeers.value
|
||||||
fun getNicknameValue() = _nickname.value
|
fun getNicknameValue() = _nickname.value
|
||||||
fun getPrivateChatsValue() = _privateChats.value ?: emptyMap()
|
fun getPrivateChatsValue() = _privateChats.value
|
||||||
fun getSelectedPrivateChatPeerValue() = _selectedPrivateChatPeer.value
|
fun getSelectedPrivateChatPeerValue() = _selectedPrivateChatPeer.value
|
||||||
fun getUnreadPrivateMessagesValue() = _unreadPrivateMessages.value ?: emptySet()
|
fun getUnreadPrivateMessagesValue() = _unreadPrivateMessages.value
|
||||||
fun getJoinedChannelsValue() = _joinedChannels.value ?: emptySet()
|
fun getJoinedChannelsValue() = _joinedChannels.value
|
||||||
// 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 getCurrentChannelValue() = _currentChannel.value
|
fun getCurrentChannelValue() = _currentChannel.value
|
||||||
fun getChannelMessagesValue() = _channelMessages.value ?: emptyMap()
|
fun getChannelMessagesValue() = _channelMessages.value
|
||||||
fun getUnreadChannelMessagesValue() = _unreadChannelMessages.value ?: emptyMap()
|
fun getUnreadChannelMessagesValue() = _unreadChannelMessages.value
|
||||||
fun getPasswordProtectedChannelsValue() = _passwordProtectedChannels.value ?: emptySet()
|
fun getPasswordProtectedChannelsValue() = _passwordProtectedChannels.value
|
||||||
fun getShowPasswordPromptValue() = _showPasswordPrompt.value ?: false
|
fun getShowPasswordPromptValue() = _showPasswordPrompt.value
|
||||||
fun getPasswordPromptChannelValue() = _passwordPromptChannel.value
|
fun getPasswordPromptChannelValue() = _passwordPromptChannel.value
|
||||||
fun getShowSidebarValue() = _showSidebar.value ?: false
|
fun getShowSidebarValue() = _showSidebar.value
|
||||||
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value ?: false
|
fun getShowCommandSuggestionsValue() = _showCommandSuggestions.value
|
||||||
fun getCommandSuggestionsValue() = _commandSuggestions.value ?: emptyList()
|
fun getCommandSuggestionsValue() = _commandSuggestions.value
|
||||||
fun getShowMentionSuggestionsValue() = _showMentionSuggestions.value ?: false
|
fun getShowMentionSuggestionsValue() = _showMentionSuggestions.value
|
||||||
fun getMentionSuggestionsValue() = _mentionSuggestions.value ?: emptyList()
|
fun getMentionSuggestionsValue() = _mentionSuggestions.value
|
||||||
fun getFavoritePeersValue() = _favoritePeers.value ?: emptySet()
|
fun getFavoritePeersValue() = _favoritePeers.value
|
||||||
fun getPeerSessionStatesValue() = _peerSessionStates.value ?: emptyMap()
|
fun getPeerSessionStatesValue() = _peerSessionStates.value
|
||||||
fun getPeerFingerprintsValue() = _peerFingerprints.value ?: emptyMap()
|
fun getPeerFingerprintsValue() = _peerFingerprints.value
|
||||||
fun getShowAppInfoValue() = _showAppInfo.value ?: false
|
fun getShowAppInfoValue() = _showAppInfo.value
|
||||||
fun getGeohashPeopleValue() = _geohashPeople.value ?: emptyList()
|
fun getGeohashPeopleValue() = _geohashPeople.value
|
||||||
fun getTeleportedGeoValue() = _teleportedGeo.value ?: emptySet()
|
fun getTeleportedGeoValue() = _teleportedGeo.value
|
||||||
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value ?: emptyMap()
|
fun getGeohashParticipantCountsValue() = _geohashParticipantCounts.value
|
||||||
|
|
||||||
// Setters for state updates
|
// Setters for state updates
|
||||||
fun setMessages(messages: List<BitchatMessage>) {
|
fun setMessages(messages: List<BitchatMessage>) {
|
||||||
@@ -197,7 +195,7 @@ class ChatState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun postTeleportedGeo(teleported: Set<String>) {
|
fun postTeleportedGeo(teleported: Set<String>) {
|
||||||
_teleportedGeo.postValue(teleported)
|
_teleportedGeo.value = teleported
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNickname(nickname: String) {
|
fun setNickname(nickname: String) {
|
||||||
@@ -269,7 +267,7 @@ class ChatState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setFavoritePeers(favorites: Set<String>) {
|
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", "setFavoritePeers called with ${favorites.size} favorites: $favorites")
|
||||||
Log.d("ChatState", "Current value: $currentValue")
|
Log.d("ChatState", "Current value: $currentValue")
|
||||||
Log.d("ChatState", "Values equal: ${currentValue == favorites}")
|
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
|
// Always set the value - even if equal, this ensures observers are triggered
|
||||||
_favoritePeers.value = favorites
|
_favoritePeers.value = favorites
|
||||||
|
|
||||||
Log.d("ChatState", "LiveData value after set: ${_favoritePeers.value}")
|
Log.d("ChatState", "StateFlow value after set: ${_favoritePeers.value}")
|
||||||
Log.d("ChatState", "LiveData has active observers: ${_favoritePeers.hasActiveObservers()}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPeerSessionStates(states: Map<String, String>) {
|
fun setPeerSessionStates(states: Map<String, String>) {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import android.app.Application
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
@@ -47,7 +47,9 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - State management
|
// MARK: - State management
|
||||||
private val state = ChatState()
|
private val state = ChatState(
|
||||||
|
scope = viewModelScope,
|
||||||
|
)
|
||||||
|
|
||||||
// Transfer progress tracking
|
// Transfer progress tracking
|
||||||
private val transferMessageMap = mutableMapOf<String, String>()
|
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 messages: StateFlow<List<BitchatMessage>> = state.messages
|
||||||
val connectedPeers: LiveData<List<String>> = state.connectedPeers
|
val connectedPeers: StateFlow<List<String>> = state.connectedPeers
|
||||||
val nickname: LiveData<String> = state.nickname
|
val nickname: StateFlow<String> = state.nickname
|
||||||
val isConnected: LiveData<Boolean> = state.isConnected
|
val isConnected: StateFlow<Boolean> = state.isConnected
|
||||||
val privateChats: LiveData<Map<String, List<BitchatMessage>>> = state.privateChats
|
val privateChats: StateFlow<Map<String, List<BitchatMessage>>> = state.privateChats
|
||||||
val selectedPrivateChatPeer: LiveData<String?> = state.selectedPrivateChatPeer
|
val selectedPrivateChatPeer: StateFlow<String?> = state.selectedPrivateChatPeer
|
||||||
val unreadPrivateMessages: LiveData<Set<String>> = state.unreadPrivateMessages
|
val unreadPrivateMessages: StateFlow<Set<String>> = state.unreadPrivateMessages
|
||||||
val joinedChannels: LiveData<Set<String>> = state.joinedChannels
|
val joinedChannels: StateFlow<Set<String>> = state.joinedChannels
|
||||||
val currentChannel: LiveData<String?> = state.currentChannel
|
val currentChannel: StateFlow<String?> = state.currentChannel
|
||||||
val channelMessages: LiveData<Map<String, List<BitchatMessage>>> = state.channelMessages
|
val channelMessages: StateFlow<Map<String, List<BitchatMessage>>> = state.channelMessages
|
||||||
val unreadChannelMessages: LiveData<Map<String, Int>> = state.unreadChannelMessages
|
val unreadChannelMessages: StateFlow<Map<String, Int>> = state.unreadChannelMessages
|
||||||
val passwordProtectedChannels: LiveData<Set<String>> = state.passwordProtectedChannels
|
val passwordProtectedChannels: StateFlow<Set<String>> = state.passwordProtectedChannels
|
||||||
val showPasswordPrompt: LiveData<Boolean> = state.showPasswordPrompt
|
val showPasswordPrompt: StateFlow<Boolean> = state.showPasswordPrompt
|
||||||
val passwordPromptChannel: LiveData<String?> = state.passwordPromptChannel
|
val passwordPromptChannel: StateFlow<String?> = state.passwordPromptChannel
|
||||||
val showSidebar: LiveData<Boolean> = state.showSidebar
|
val showSidebar: StateFlow<Boolean> = state.showSidebar
|
||||||
val hasUnreadChannels = state.hasUnreadChannels
|
val hasUnreadChannels = state.hasUnreadChannels
|
||||||
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
|
val hasUnreadPrivateMessages = state.hasUnreadPrivateMessages
|
||||||
val showCommandSuggestions: LiveData<Boolean> = state.showCommandSuggestions
|
val showCommandSuggestions: StateFlow<Boolean> = state.showCommandSuggestions
|
||||||
val commandSuggestions: LiveData<List<CommandSuggestion>> = state.commandSuggestions
|
val commandSuggestions: StateFlow<List<CommandSuggestion>> = state.commandSuggestions
|
||||||
val showMentionSuggestions: LiveData<Boolean> = state.showMentionSuggestions
|
val showMentionSuggestions: StateFlow<Boolean> = state.showMentionSuggestions
|
||||||
val mentionSuggestions: LiveData<List<String>> = state.mentionSuggestions
|
val mentionSuggestions: StateFlow<List<String>> = state.mentionSuggestions
|
||||||
val favoritePeers: LiveData<Set<String>> = state.favoritePeers
|
val favoritePeers: StateFlow<Set<String>> = state.favoritePeers
|
||||||
val peerSessionStates: LiveData<Map<String, String>> = state.peerSessionStates
|
val peerSessionStates: StateFlow<Map<String, String>> = state.peerSessionStates
|
||||||
val peerFingerprints: LiveData<Map<String, String>> = state.peerFingerprints
|
val peerFingerprints: StateFlow<Map<String, String>> = state.peerFingerprints
|
||||||
val peerNicknames: LiveData<Map<String, String>> = state.peerNicknames
|
val peerNicknames: StateFlow<Map<String, String>> = state.peerNicknames
|
||||||
val peerRSSI: LiveData<Map<String, Int>> = state.peerRSSI
|
val peerRSSI: StateFlow<Map<String, Int>> = state.peerRSSI
|
||||||
val peerDirect: LiveData<Map<String, Boolean>> = state.peerDirect
|
val peerDirect: StateFlow<Map<String, Boolean>> = state.peerDirect
|
||||||
val showAppInfo: LiveData<Boolean> = state.showAppInfo
|
val showAppInfo: StateFlow<Boolean> = state.showAppInfo
|
||||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||||
val isTeleported: LiveData<Boolean> = state.isTeleported
|
val isTeleported: StateFlow<Boolean> = state.isTeleported
|
||||||
val geohashPeople: LiveData<List<GeoPerson>> = state.geohashPeople
|
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
||||||
val teleportedGeo: LiveData<Set<String>> = state.teleportedGeo
|
val teleportedGeo: StateFlow<Set<String>> = state.teleportedGeo
|
||||||
val geohashParticipantCounts: LiveData<Map<String, Int>> = state.geohashParticipantCounts
|
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Note: Mesh service delegate is now set by MainActivity
|
// Note: Mesh service delegate is now set by MainActivity
|
||||||
@@ -565,7 +567,7 @@ class ChatViewModel(
|
|||||||
|
|
||||||
private fun logCurrentFavoriteState() {
|
private fun logCurrentFavoriteState() {
|
||||||
Log.i("ChatViewModel", "=== CURRENT FAVORITE STATE ===")
|
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", "DataManager favorite peers: ${dataManager.favoritePeers}")
|
||||||
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
|
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
|
||||||
Log.i("ChatViewModel", "==============================")
|
Log.i("ChatViewModel", "==============================")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.outlined.Explore
|
|||||||
import androidx.compose.material.icons.outlined.LocationOn
|
import androidx.compose.material.icons.outlined.LocationOn
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -21,6 +20,7 @@ import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,11 +46,11 @@ fun GeohashPeopleList(
|
|||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
|
||||||
// Observe geohash people from ChatViewModel
|
// Observe geohash people from ChatViewModel
|
||||||
val geohashPeople by viewModel.geohashPeople.observeAsState(emptyList())
|
val geohashPeople by viewModel.geohashPeople.collectAsStateWithLifecycle()
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
val isTeleported by viewModel.isTeleported.observeAsState(false)
|
val isTeleported by viewModel.isTeleported.collectAsStateWithLifecycle()
|
||||||
val nickname by viewModel.nickname.observeAsState("")
|
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||||
val unreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
val unreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
// Header matching iOS style
|
// Header matching iOS style
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.bitchat.android.ui
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitchat.android.nostr.GeohashMessageHandler
|
import com.bitchat.android.nostr.GeohashMessageHandler
|
||||||
import com.bitchat.android.nostr.GeohashRepository
|
import com.bitchat.android.nostr.GeohashRepository
|
||||||
@@ -15,6 +14,7 @@ import com.bitchat.android.nostr.NostrSubscriptionManager
|
|||||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@@ -55,9 +55,9 @@ class GeohashViewModel(
|
|||||||
private var geoTimer: Job? = null
|
private var geoTimer: Job? = null
|
||||||
private var locationChannelManager: com.bitchat.android.geohash.LocationChannelManager? = null
|
private var locationChannelManager: com.bitchat.android.geohash.LocationChannelManager? = null
|
||||||
|
|
||||||
val geohashPeople: LiveData<List<GeoPerson>> = state.geohashPeople
|
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
|
||||||
val geohashParticipantCounts: LiveData<Map<String, Int>> = state.geohashParticipantCounts
|
val geohashParticipantCounts: StateFlow<Map<String, Int>> = state.geohashParticipantCounts
|
||||||
val selectedLocationChannel: LiveData<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
subscriptionManager.connect()
|
subscriptionManager.connect()
|
||||||
@@ -73,13 +73,17 @@ class GeohashViewModel(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
locationChannelManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
|
locationChannelManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
|
||||||
locationChannelManager?.selectedChannel?.observeForever { channel ->
|
viewModelScope.launch {
|
||||||
|
locationChannelManager?.selectedChannel?.collect { channel ->
|
||||||
state.setSelectedLocationChannel(channel)
|
state.setSelectedLocationChannel(channel)
|
||||||
switchLocationChannel(channel)
|
switchLocationChannel(channel)
|
||||||
}
|
}
|
||||||
locationChannelManager?.teleported?.observeForever { teleported ->
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
locationChannelManager?.teleported?.collect { teleported ->
|
||||||
state.setIsTeleported(teleported)
|
state.setIsTeleported(teleported)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initialize location channel state: ${e.message}")
|
Log.e(TAG, "Failed to initialize location channel state: ${e.message}")
|
||||||
state.setSelectedLocationChannel(com.bitchat.android.geohash.ChannelID.Mesh)
|
state.setSelectedLocationChannel(com.bitchat.android.geohash.ChannelID.Mesh)
|
||||||
@@ -120,7 +124,7 @@ class GeohashViewModel(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val identity = NostrIdentityBridge.deriveIdentity(forGeohash = channel.geohash, context = getApplication())
|
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 event = NostrProtocol.createEphemeralGeohashEvent(content, channel.geohash, identity, nickname, teleported)
|
||||||
val relayManager = NostrRelayManager.getInstance(getApplication())
|
val relayManager = NostrRelayManager.getInstance(getApplication())
|
||||||
relayManager.sendEventToGeohash(event, channel.geohash, includeDefaults = false, nRelays = 5)
|
relayManager.sendEventToGeohash(event, channel.geohash, includeDefaults = false, nRelays = 5)
|
||||||
@@ -231,7 +235,7 @@ class GeohashViewModel(
|
|||||||
try {
|
try {
|
||||||
val identity = NostrIdentityBridge.deriveIdentity(channel.channel.geohash, getApplication())
|
val identity = NostrIdentityBridge.deriveIdentity(channel.channel.geohash, getApplication())
|
||||||
repo.updateParticipant(channel.channel.geohash, identity.publicKeyHex, Date())
|
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)
|
if (teleported) repo.markTeleported(identity.publicKeyHex)
|
||||||
} catch (e: Exception) { Log.w(TAG, "Failed identity setup: ${e.message}") }
|
} 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.material.icons.outlined.BookmarkBorder
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
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.geohash.GeohashBookmarksStore
|
||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
|
import com.bitchat.android.core.ui.component.button.CloseButton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Location Channels Sheet for selecting geohash-based location channels
|
* Location Channels Sheet for selecting geohash-based location channels
|
||||||
@@ -57,18 +58,18 @@ fun LocationChannelsSheet(
|
|||||||
val bookmarksStore = remember { GeohashBookmarksStore.getInstance(context) }
|
val bookmarksStore = remember { GeohashBookmarksStore.getInstance(context) }
|
||||||
|
|
||||||
// Observe location manager state
|
// Observe location manager state
|
||||||
val permissionState by locationManager.permissionState.observeAsState()
|
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||||
val availableChannels by locationManager.availableChannels.observeAsState(emptyList())
|
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||||
val selectedChannel by locationManager.selectedChannel.observeAsState()
|
val selectedChannel by locationManager.selectedChannel.collectAsStateWithLifecycle()
|
||||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||||
val locationServicesEnabled by locationManager.locationServicesEnabled.observeAsState(false)
|
val locationServicesEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Observe bookmarks state
|
// Observe bookmarks state
|
||||||
val bookmarks by bookmarksStore.bookmarks.observeAsState(emptyList())
|
val bookmarks by bookmarksStore.bookmarks.collectAsStateWithLifecycle()
|
||||||
val bookmarkNames by bookmarksStore.bookmarkNames.observeAsState(emptyMap())
|
val bookmarkNames by bookmarksStore.bookmarkNames.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Observe reactive participant counts
|
// Observe reactive participant counts
|
||||||
val geohashParticipantCounts by viewModel.geohashParticipantCounts.observeAsState(emptyMap())
|
val geohashParticipantCounts by viewModel.geohashParticipantCounts.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
var customGeohash by remember { mutableStateOf("") }
|
var customGeohash by remember { mutableStateOf("") }
|
||||||
@@ -551,22 +552,16 @@ fun LocationChannelsSheet(
|
|||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
|
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
|
||||||
) {
|
) {
|
||||||
TextButton(
|
CloseButton(
|
||||||
onClick = onDismiss,
|
onClick = onDismiss,
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp),
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.close_plain),
|
|
||||||
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
|
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle management: when presented, sample both nearby and bookmarked geohashes
|
// Lifecycle management: when presented, sample both nearby and bookmarked geohashes
|
||||||
LaunchedEffect(isPresented, availableChannels, bookmarks) {
|
LaunchedEffect(isPresented, availableChannels, bookmarks) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -35,10 +35,10 @@ fun LocationNotesButton(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
// Get channel and permission state
|
// Get channel and permission state
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
|
||||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||||
val permissionState by locationManager.permissionState.observeAsState()
|
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
|
||||||
val locationServicesEnabled by locationManager.locationServicesEnabled.observeAsState(false)
|
val locationServicesEnabled by locationManager.locationServicesEnabled.collectAsStateWithLifecycle(false)
|
||||||
|
|
||||||
// Check both permission AND location services enabled
|
// Check both permission AND location services enabled
|
||||||
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
|
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
|
||||||
@@ -46,7 +46,7 @@ fun LocationNotesButton(
|
|||||||
|
|
||||||
// Get notes count from LocationNotesManager
|
// Get notes count from LocationNotesManager
|
||||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||||
val notes by notesManager.notes.observeAsState(emptyList())
|
val notes by notesManager.notes.collectAsStateWithLifecycle()
|
||||||
val notesCount = notes.size
|
val notesCount = notes.size
|
||||||
|
|
||||||
// Only show in mesh mode when location is authorized (iOS pattern)
|
// 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.material.icons.filled.ArrowUpward
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||||
import com.bitchat.android.geohash.LocationChannelManager
|
import com.bitchat.android.geohash.LocationChannelManager
|
||||||
import com.bitchat.android.nostr.LocationNotesManager
|
import com.bitchat.android.nostr.LocationNotesManager
|
||||||
@@ -57,16 +57,16 @@ fun LocationNotesSheet(
|
|||||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||||
|
|
||||||
// State
|
// State
|
||||||
val notes by notesManager.notes.observeAsState(emptyList())
|
val notes by notesManager.notes.collectAsStateWithLifecycle()
|
||||||
val state by notesManager.state.observeAsState(LocationNotesManager.State.IDLE)
|
val state by notesManager.state.collectAsStateWithLifecycle(LocationNotesManager.State.IDLE)
|
||||||
val errorMessage by notesManager.errorMessage.observeAsState()
|
val errorMessage by notesManager.errorMessage.collectAsStateWithLifecycle()
|
||||||
val initialLoadComplete by notesManager.initialLoadComplete.observeAsState(false)
|
val initialLoadComplete by notesManager.initialLoadComplete.collectAsStateWithLifecycle(false)
|
||||||
|
|
||||||
// SIMPLIFIED: Get count directly from notes list (no separate counter needed)
|
// SIMPLIFIED: Get count directly from notes list (no separate counter needed)
|
||||||
val count = notes.size
|
val count = notes.size
|
||||||
|
|
||||||
// Get location name (building or block) - matches iOS locationNames lookup
|
// 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() }
|
val displayLocationName = locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() }
|
||||||
?: locationNames[GeohashChannelLevel.BLOCK]?.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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.geohash.GeohashChannelLevel
|
import com.bitchat.android.geohash.GeohashChannelLevel
|
||||||
import com.bitchat.android.geohash.LocationChannelManager
|
import com.bitchat.android.geohash.LocationChannelManager
|
||||||
|
|
||||||
@@ -26,15 +26,15 @@ fun LocationNotesSheetPresenter(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||||
val availableChannels by locationManager.availableChannels.observeAsState(emptyList())
|
val availableChannels by locationManager.availableChannels.collectAsStateWithLifecycle()
|
||||||
val nickname by viewModel.nickname.observeAsState("")
|
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
// iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
|
||||||
val buildingGeohash = availableChannels.firstOrNull { it.level == GeohashChannelLevel.BUILDING }?.geohash
|
val buildingGeohash = availableChannels.firstOrNull { it.level == GeohashChannelLevel.BUILDING }?.geohash
|
||||||
|
|
||||||
if (buildingGeohash != null) {
|
if (buildingGeohash != null) {
|
||||||
// Get location name from locationManager
|
// Get location name from locationManager
|
||||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
val locationNames by locationManager.locationNames.collectAsStateWithLifecycle()
|
||||||
val locationName = locationNames[GeohashChannelLevel.BUILDING]
|
val locationName = locationNames[GeohashChannelLevel.BUILDING]
|
||||||
?: locationNames[GeohashChannelLevel.BLOCK]
|
?: locationNames[GeohashChannelLevel.BLOCK]
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -26,7 +27,7 @@ private enum class CharacterAnimationState {
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun shouldAnimateMessage(messageId: String): Boolean {
|
fun shouldAnimateMessage(messageId: String): Boolean {
|
||||||
val miningMessages by PoWMiningTracker.miningMessages.collectAsState()
|
val miningMessages by PoWMiningTracker.miningMessages.collectAsStateWithLifecycle()
|
||||||
return miningMessages.contains(messageId)
|
return miningMessages.contains(messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.bitchat.android.nostr.NostrProofOfWork
|
import com.bitchat.android.nostr.NostrProofOfWork
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.R
|
import com.bitchat.android.R
|
||||||
import com.bitchat.android.nostr.PoWPreferenceManager
|
import com.bitchat.android.nostr.PoWPreferenceManager
|
||||||
|
|
||||||
@@ -27,9 +28,9 @@ fun PoWStatusIndicator(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
style: PoWIndicatorStyle = PoWIndicatorStyle.COMPACT
|
style: PoWIndicatorStyle = PoWIndicatorStyle.COMPACT
|
||||||
) {
|
) {
|
||||||
val powEnabled by PoWPreferenceManager.powEnabled.collectAsState()
|
val powEnabled by PoWPreferenceManager.powEnabled.collectAsStateWithLifecycle()
|
||||||
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState()
|
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsStateWithLifecycle()
|
||||||
val isMining by PoWPreferenceManager.isMining.collectAsState()
|
val isMining by PoWPreferenceManager.isMining.collectAsStateWithLifecycle()
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
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.material.icons.outlined.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
|
||||||
|
|
||||||
|
|
||||||
@@ -39,14 +39,14 @@ fun SidebarOverlay(
|
|||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
||||||
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
val connectedPeers by viewModel.connectedPeers.collectAsStateWithLifecycle()
|
||||||
val joinedChannels by viewModel.joinedChannels.observeAsState(emptyList())
|
val joinedChannels by viewModel.joinedChannels.collectAsStateWithLifecycle()
|
||||||
val currentChannel by viewModel.currentChannel.observeAsState()
|
val currentChannel by viewModel.currentChannel.collectAsStateWithLifecycle()
|
||||||
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.observeAsState()
|
val selectedPrivatePeer by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
|
||||||
val nickname by viewModel.nickname.observeAsState("")
|
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
|
||||||
val unreadChannelMessages by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
val unreadChannelMessages by viewModel.unreadChannelMessages.collectAsStateWithLifecycle()
|
||||||
val peerNicknames by viewModel.peerNicknames.observeAsState(emptyMap())
|
val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle()
|
||||||
val peerRSSI by viewModel.peerRSSI.observeAsState(emptyMap())
|
val peerRSSI by viewModel.peerRSSI.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -110,7 +110,7 @@ fun SidebarOverlay(
|
|||||||
|
|
||||||
// People section - switch between mesh and geohash lists (iOS-compatible)
|
// People section - switch between mesh and geohash lists (iOS-compatible)
|
||||||
item {
|
item {
|
||||||
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
|
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsState()
|
||||||
|
|
||||||
when (selectedLocationChannel) {
|
when (selectedLocationChannel) {
|
||||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||||
@@ -291,10 +291,10 @@ fun PeopleSection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Observe reactive state for favorites and fingerprints
|
// Observe reactive state for favorites and fingerprints
|
||||||
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.collectAsStateWithLifecycle()
|
||||||
val privateChats by viewModel.privateChats.observeAsState(emptyMap())
|
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
|
||||||
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
|
||||||
val peerFingerprints by viewModel.peerFingerprints.observeAsState(emptyMap())
|
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
// Reactive favorite computation for all peers
|
// Reactive favorite computation for all peers
|
||||||
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
|
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
|
||||||
@@ -384,7 +384,7 @@ fun PeopleSection(
|
|||||||
val (bName, _) = com.bitchat.android.ui.splitSuffix(displayName)
|
val (bName, _) = com.bitchat.android.ui.splitSuffix(displayName)
|
||||||
val showHash = (baseNameCounts[bName] ?: 0) > 1
|
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 }
|
val isDirectLive = directMap[peerID] ?: try { viewModel.meshService.getPeerInfo(peerID)?.isDirectConnection == true } catch (_: Exception) { false }
|
||||||
PeerItem(
|
PeerItem(
|
||||||
peerID = peerID,
|
peerID = peerID,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import androidx.test.core.app.ApplicationProvider
|
|||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import junit.framework.TestCase.assertEquals
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -18,7 +20,10 @@ import java.util.Date
|
|||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class CommandProcessorTest() {
|
class CommandProcessorTest() {
|
||||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||||
private val chatState = ChatState()
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val testDispatcher = UnconfinedTestDispatcher()
|
||||||
|
private val testScope = TestScope(testDispatcher)
|
||||||
|
private val chatState = ChatState(scope = testScope)
|
||||||
private lateinit var commandProcessor: CommandProcessor
|
private lateinit var commandProcessor: CommandProcessor
|
||||||
|
|
||||||
val messageManager: MessageManager = MessageManager(state = chatState)
|
val messageManager: MessageManager = MessageManager(state = chatState)
|
||||||
@@ -26,7 +31,7 @@ class CommandProcessorTest() {
|
|||||||
state = chatState,
|
state = chatState,
|
||||||
messageManager = messageManager,
|
messageManager = messageManager,
|
||||||
dataManager = DataManager(context = context),
|
dataManager = DataManager(context = context),
|
||||||
coroutineScope = kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main.immediate)
|
coroutineScope = testScope
|
||||||
)
|
)
|
||||||
|
|
||||||
private val meshService: BluetoothMeshService = mock()
|
private val meshService: BluetoothMeshService = mock()
|
||||||
|
|||||||
@@ -68,12 +68,10 @@ androidx-compose-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
|
|||||||
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||||
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||||
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
androidx-compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||||
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
|
|
||||||
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
|
||||||
|
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-runtime" }
|
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-runtime" }
|
||||||
androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle-runtime" }
|
|
||||||
|
|
||||||
# Navigation
|
# Navigation
|
||||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
|
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
|
||||||
@@ -130,14 +128,12 @@ compose = [
|
|||||||
"androidx-compose-ui-graphics",
|
"androidx-compose-ui-graphics",
|
||||||
"androidx-compose-ui-tooling-preview",
|
"androidx-compose-ui-tooling-preview",
|
||||||
"androidx-compose-material3",
|
"androidx-compose-material3",
|
||||||
"androidx-compose-runtime-livedata",
|
|
||||||
"androidx-compose-material-icons-extended"
|
"androidx-compose-material-icons-extended"
|
||||||
]
|
]
|
||||||
|
|
||||||
lifecycle = [
|
lifecycle = [
|
||||||
"androidx-lifecycle-runtime-ktx",
|
"androidx-lifecycle-runtime-ktx",
|
||||||
"androidx-lifecycle-viewmodel-compose",
|
"androidx-lifecycle-viewmodel-compose",
|
||||||
"androidx-lifecycle-livedata-ktx"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
cryptography = [
|
cryptography = [
|
||||||
|
|||||||
Reference in New Issue
Block a user