This commit is contained in:
callebtc
2025-10-18 01:04:24 +02:00
parent c1633914bb
commit c3a1cc5513
8 changed files with 315 additions and 179 deletions
@@ -598,42 +598,8 @@ class MainActivity : ComponentActivity() {
PoWPreferenceManager.init(this@MainActivity)
Log.d("MainActivity", "PoW preferences initialized")
// SIMPLIFIED: Initialize Location Notes Manager only (no separate counter)
com.bitchat.android.nostr.LocationNotesManager.getInstance().initialize(
relayManager = { com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity) },
subscribe = { filter, id, handler ->
// CRITICAL FIX: Extract geohash properly from filter using getGeohash() method
val geohashFromFilter = filter.getGeohash() ?: run {
Log.e("MainActivity", "❌ Cannot extract geohash from filter for location notes")
return@initialize id // Return subscription ID even on error
}
Log.d("MainActivity", "📍 Location Notes subscribing to geohash: $geohashFromFilter")
com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity).subscribeForGeohash(
geohash = geohashFromFilter,
filter = filter,
id = id,
handler = handler,
includeDefaults = true,
nRelays = 5
)
},
unsubscribe = { id ->
com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity).unsubscribe(id)
},
sendEvent = { event, relayUrls ->
if (relayUrls != null) {
com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity).sendEvent(event, relayUrls)
} else {
com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity).sendEvent(event)
}
},
deriveIdentity = { geohash ->
com.bitchat.android.nostr.NostrIdentityBridge.deriveIdentity(geohash, this@MainActivity)
}
)
Log.d("MainActivity", "✅ Location Notes Manager initialized")
// Initialize Location Notes Manager (extracted to separate file)
com.bitchat.android.nostr.LocationNotesInitializer.initialize(this@MainActivity)
// Ensure all permissions are still granted (user might have revoked in settings)
if (!permissionManager.areAllPermissionsGranted()) {
@@ -0,0 +1,63 @@
package com.bitchat.android.nostr
import android.content.Context
import android.util.Log
/**
* Initializer for LocationNotesManager with all dependencies
* Extracts initialization logic from MainActivity for better separation of concerns
*/
object LocationNotesInitializer {
private const val TAG = "LocationNotesInitializer"
/**
* Initialize LocationNotesManager with all required dependencies
*
* @param context Application context
* @return true if initialization succeeded, false otherwise
*/
fun initialize(context: Context): Boolean {
return try {
LocationNotesManager.getInstance().initialize(
relayManager = { NostrRelayManager.getInstance(context) },
subscribe = { filter, id, handler ->
// CRITICAL FIX: Extract geohash properly from filter using getGeohash() method
val geohashFromFilter = filter.getGeohash() ?: run {
Log.e(TAG, "❌ Cannot extract geohash from filter for location notes")
return@initialize id // Return subscription ID even on error
}
Log.d(TAG, "📍 Location Notes subscribing to geohash: $geohashFromFilter")
NostrRelayManager.getInstance(context).subscribeForGeohash(
geohash = geohashFromFilter,
filter = filter,
id = id,
handler = handler,
includeDefaults = true,
nRelays = 5
)
},
unsubscribe = { id ->
NostrRelayManager.getInstance(context).unsubscribe(id)
},
sendEvent = { event, relayUrls ->
if (relayUrls != null) {
NostrRelayManager.getInstance(context).sendEvent(event, relayUrls)
} else {
NostrRelayManager.getInstance(context).sendEvent(event)
}
},
deriveIdentity = { geohash ->
NostrIdentityBridge.deriveIdentity(geohash, context)
}
)
Log.d(TAG, "✅ Location Notes Manager initialized")
true
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to initialize Location Notes Manager: ${e.message}", e)
false
}
}
}
@@ -535,16 +535,6 @@ private fun MainHeader(
val context = androidx.compose.ui.platform.LocalContext.current
val bookmarksStore = remember { com.bitchat.android.geohash.GeohashBookmarksStore.getInstance(context) }
val bookmarks by bookmarksStore.bookmarks.observeAsState(emptyList())
// SIMPLIFIED: Get notes count directly from LocationNotesManager (no separate counter)
val notesManager = remember { com.bitchat.android.nostr.LocationNotesManager.getInstance() }
val notes by notesManager.notes.observeAsState(emptyList())
val notesCount = notes.size
// Location channel manager for permission state
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
val permissionState by locationManager.permissionState.observeAsState()
val locationPermissionGranted = permissionState == PermissionState.AUTHORIZED
Row(
modifier = Modifier.fillMaxWidth(),
@@ -623,22 +613,11 @@ private fun MainHeader(
}
}
// Location Notes button (mesh only, when location is authorized)
// iOS: Shows to the left of #mesh badge when in mesh mode and location permitted
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Mesh && locationPermissionGranted) {
val hasNotes = (notesCount ?: 0) > 0
IconButton(
onClick = onLocationNotesClick,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Outlined.Description, // "long.text.page.and.pencil" equivalent
contentDescription = stringResource(R.string.cd_location_notes),
modifier = Modifier.size(16.dp),
tint = if (hasNotes) colorScheme.primary else Color.Gray
)
}
}
// Location Notes button (extracted to separate component)
LocationNotesButton(
viewModel = viewModel,
onClick = onLocationNotesClick
)
// Tor status dot when Tor is enabled
TorStatusDot(
@@ -539,61 +539,12 @@ private fun ChatDialogs(
)
}
// Location notes sheet - matches iOS pattern exactly
// Location notes sheet (extracted to separate presenter)
if (showLocationNotesSheet) {
val context = androidx.compose.ui.platform.LocalContext.current
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
val availableChannels by locationManager.availableChannels.observeAsState(emptyList())
val nickname by viewModel.nickname.observeAsState("")
// iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
val buildingGeohash = availableChannels.firstOrNull { it.level == com.bitchat.android.geohash.GeohashChannelLevel.BUILDING }?.geohash
if (buildingGeohash != null) {
// Get location name from locationManager
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
val locationName = locationNames[com.bitchat.android.geohash.GeohashChannelLevel.BUILDING]
?: locationNames[com.bitchat.android.geohash.GeohashChannelLevel.BLOCK]
LocationNotesSheet(
geohash = buildingGeohash,
locationName = locationName,
nickname = nickname,
onDismiss = onLocationNotesSheetDismiss
)
} else {
// No building geohash available - show error state (matches iOS)
ModalBottomSheet(
onDismissRequest = onLocationNotesSheetDismiss,
containerColor = MaterialTheme.colorScheme.surface
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Location Unavailable",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Location permission is required for notes",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = {
locationManager.enableLocationChannels()
locationManager.refreshChannels()
}) {
Text("Enable Location")
}
}
}
}
LocationNotesSheetPresenter(
viewModel = viewModel,
onDismiss = onLocationNotesSheetDismiss
)
}
// User action sheet
@@ -594,69 +594,7 @@ class ChatViewModel(
}
}
/**
* SIMPLIFIED: Initialize location notes manager subscription (no separate counter)
* Subscribes/unsubscribes based on channel selection and location permission
*/
private fun initializeLocationNotesManagerSubscription() {
viewModelScope.launch {
// Observe channel changes
selectedLocationChannel.observeForever { channel ->
updateLocationNotesSubscription(channel)
}
}
}
/**
* SIMPLIFIED: Update location notes subscription based on current channel and location permission
* iOS pattern: Subscribe when in mesh mode and location is authorized
*/
private fun updateLocationNotesSubscription(channel: com.bitchat.android.geohash.ChannelID?) {
try {
val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
val permissionState = locationManager.permissionState.value
val locationPermissionGranted = permissionState == com.bitchat.android.geohash.LocationChannelManager.PermissionState.AUTHORIZED
when (channel) {
is com.bitchat.android.geohash.ChannelID.Mesh -> {
// Mesh mode: subscribe to building-level geohash notes if location authorized
if (locationPermissionGranted) {
// Refresh location to get current building geohash
locationManager.refreshChannels()
// Get building-level geohash (precision 8, same as iOS)
val buildingGeohash = locationManager.availableChannels.value
?.firstOrNull { it.level == com.bitchat.android.geohash.GeohashChannelLevel.BUILDING }
?.geohash
if (buildingGeohash != null) {
Log.d(TAG, "📍 Subscribing to location notes for building geohash: $buildingGeohash")
com.bitchat.android.nostr.LocationNotesManager.getInstance().setGeohash(buildingGeohash)
} else {
// Cancel if no building geohash available
if (com.bitchat.android.nostr.LocationNotesManager.getInstance().geohash.value == null) {
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
}
}
} else {
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
}
}
is com.bitchat.android.geohash.ChannelID.Location -> {
// Location channel mode: cancel (only show in mesh mode)
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
}
null -> {
// Default to mesh behavior
if (locationPermissionGranted) {
locationManager.refreshChannels()
}
}
}
} catch (e: Exception) {
Log.w(TAG, "updateLocationNotesSubscription failed: ${e.message}")
}
}
// Location notes subscription management moved to LocationNotesViewModelExtensions.kt
/**
* Update reactive states for all connected peers (session states, fingerprints, nicknames, RSSI)
@@ -0,0 +1,63 @@
package com.bitchat.android.ui
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitchat.android.R
import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.geohash.LocationChannelManager
import com.bitchat.android.nostr.LocationNotesManager
/**
* Location Notes button component for MainHeader
* Displays when in mesh mode and location permission is granted
* Icon turns primary color when notes exist, gray otherwise
*/
@Composable
fun LocationNotesButton(
viewModel: ChatViewModel,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val colorScheme = MaterialTheme.colorScheme
val context = LocalContext.current
// Get channel and permission state
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
val locationManager = remember { LocationChannelManager.getInstance(context) }
val permissionState by locationManager.permissionState.observeAsState()
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
// Get notes count from LocationNotesManager
val notesManager = remember { LocationNotesManager.getInstance() }
val notes by notesManager.notes.observeAsState(emptyList())
val notesCount = notes.size
// Only show in mesh mode when location is authorized (iOS pattern)
if (selectedLocationChannel is ChannelID.Mesh && locationPermissionGranted) {
val hasNotes = (notesCount ?: 0) > 0
IconButton(
onClick = onClick,
modifier = modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Outlined.Description, // "long.text.page.and.pencil" equivalent
contentDescription = stringResource(R.string.cd_location_notes),
modifier = Modifier.size(16.dp),
tint = if (hasNotes) colorScheme.primary else Color.Gray
)
}
}
}
@@ -0,0 +1,95 @@
package com.bitchat.android.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.geohash.LocationChannelManager
/**
* Presenter component for LocationNotesSheet
* Handles sheet presentation logic with proper error states
* Extracts this logic from ChatScreen for better separation of concerns
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LocationNotesSheetPresenter(
viewModel: ChatViewModel,
onDismiss: () -> Unit
) {
val context = LocalContext.current
val locationManager = remember { LocationChannelManager.getInstance(context) }
val availableChannels by locationManager.availableChannels.observeAsState(emptyList())
val nickname by viewModel.nickname.observeAsState("")
// iOS pattern: notesGeohash ?? LocationChannelManager.shared.availableChannels.first(where: { $0.level == .building })?.geohash
val buildingGeohash = availableChannels.firstOrNull { it.level == GeohashChannelLevel.BUILDING }?.geohash
if (buildingGeohash != null) {
// Get location name from locationManager
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
val locationName = locationNames[GeohashChannelLevel.BUILDING]
?: locationNames[GeohashChannelLevel.BLOCK]
LocationNotesSheet(
geohash = buildingGeohash,
locationName = locationName,
nickname = nickname,
onDismiss = onDismiss
)
} else {
// No building geohash available - show error state (matches iOS)
LocationNotesErrorSheet(
onDismiss = onDismiss,
locationManager = locationManager
)
}
}
/**
* Error sheet when location is unavailable
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LocationNotesErrorSheet(
onDismiss: () -> Unit,
locationManager: LocationChannelManager
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
containerColor = MaterialTheme.colorScheme.surface
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Location Unavailable",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Location permission is required for notes",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = {
locationManager.enableLocationChannels()
locationManager.refreshChannels()
}) {
Text("Enable Location")
}
}
}
}
@@ -0,0 +1,81 @@
package com.bitchat.android.ui
import android.app.Application
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.geohash.LocationChannelManager
import com.bitchat.android.nostr.LocationNotesManager
import kotlinx.coroutines.launch
/**
* Extension functions for ChatViewModel to manage LocationNotesManager subscription
* Extracts location notes subscription logic for better separation of concerns
*/
private const val TAG = "LocationNotesVM"
/**
* Initialize location notes manager subscription (no separate counter)
* Subscribes/unsubscribes based on channel selection and location permission
*/
fun ChatViewModel.initializeLocationNotesManagerSubscription() {
viewModelScope.launch {
// Observe channel changes
selectedLocationChannel.observeForever { channel ->
updateLocationNotesSubscription(channel)
}
}
}
/**
* Update location notes subscription based on current channel and location permission
* iOS pattern: Subscribe when in mesh mode and location is authorized
*/
private fun ChatViewModel.updateLocationNotesSubscription(channel: ChannelID?) {
try {
val locationManager = LocationChannelManager.getInstance(getApplication<Application>())
val permissionState = locationManager.permissionState.value
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
when (channel) {
is ChannelID.Mesh -> {
// Mesh mode: subscribe to building-level geohash notes if location authorized
if (locationPermissionGranted) {
// Refresh location to get current building geohash
locationManager.refreshChannels()
// Get building-level geohash (precision 8, same as iOS)
val buildingGeohash = locationManager.availableChannels.value
?.firstOrNull { it.level == GeohashChannelLevel.BUILDING }
?.geohash
if (buildingGeohash != null) {
Log.d(TAG, "📍 Subscribing to location notes for building geohash: $buildingGeohash")
LocationNotesManager.getInstance().setGeohash(buildingGeohash)
} else {
// Cancel if no building geohash available
if (LocationNotesManager.getInstance().geohash.value == null) {
LocationNotesManager.getInstance().cancel()
}
}
} else {
LocationNotesManager.getInstance().cancel()
}
}
is ChannelID.Location -> {
// Location channel mode: cancel (only show in mesh mode)
LocationNotesManager.getInstance().cancel()
}
null -> {
// Default to mesh behavior
if (locationPermissionGranted) {
locationManager.refreshChannels()
}
}
}
} catch (e: Exception) {
Log.w(TAG, "updateLocationNotesSubscription failed: ${e.message}")
}
}