simplify and tor icon change

This commit is contained in:
callebtc
2025-10-18 00:07:02 +02:00
parent abb415c488
commit e6e567727d
5 changed files with 42 additions and 258 deletions
@@ -598,36 +598,7 @@ class MainActivity : ComponentActivity() {
PoWPreferenceManager.init(this@MainActivity) PoWPreferenceManager.init(this@MainActivity)
Log.d("MainActivity", "PoW preferences initialized") Log.d("MainActivity", "PoW preferences initialized")
// Initialize Location Notes Counter (iOS parity) // SIMPLIFIED: Initialize Location Notes Manager only (no separate counter)
com.bitchat.android.nostr.LocationNotesCounter.initialize(
relayManager = { com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity) },
subscribe = { filter, id, handler ->
// CRITICAL FIX: Extract geohash properly from filter (not from debug string!)
val geohashFromFilter = filter.getGeohash() ?: run {
Log.e("MainActivity", "Cannot extract geohash from filter for location notes counter")
""
}
if (geohashFromFilter.isNotEmpty()) {
com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity).subscribeForGeohash(
geohash = geohashFromFilter,
filter = filter,
id = id,
handler = handler,
includeDefaults = true,
nRelays = 5
)
} else {
id
}
},
unsubscribe = { id ->
com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity).unsubscribe(id)
}
)
Log.d("MainActivity", "Location Notes Counter initialized")
// CRITICAL FIX: Initialize Location Notes Manager (iOS parity)
com.bitchat.android.nostr.LocationNotesManager.getInstance().initialize( com.bitchat.android.nostr.LocationNotesManager.getInstance().initialize(
relayManager = { com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity) }, relayManager = { com.bitchat.android.nostr.NostrRelayManager.getInstance(this@MainActivity) },
subscribe = { filter, id, handler -> subscribe = { filter, id, handler ->
@@ -1,187 +0,0 @@
package com.bitchat.android.nostr
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
/**
* Lightweight counter for location notes
* Singleton manager for counting unique notes per geohash
* iOS-compatible implementation
*/
object LocationNotesCounter {
private const val TAG = "LocationNotesCounter"
// Published state
private val _geohash = MutableLiveData<String?>(null)
val geohash: LiveData<String?> = _geohash
private val _count = MutableLiveData(0)
val count: LiveData<Int> = _count
private val _initialLoadComplete = MutableLiveData(false)
val initialLoadComplete: LiveData<Boolean> = _initialLoadComplete
private val _relayAvailable = MutableLiveData(false)
val relayAvailable: LiveData<Boolean> = _relayAvailable
// Private state
private var subscriptionID: String? = null
private val eventIDs = mutableSetOf<String>() // For deduplication
// Dependencies (injected)
private var relayLookup: (() -> NostrRelayManager)? = null
private var subscribeFunc: ((NostrFilter, String, (NostrEvent) -> Unit) -> String)? = null
private var unsubscribeFunc: ((String) -> Unit)? = null
/**
* Initialize dependencies
*/
fun initialize(
relayManager: () -> NostrRelayManager,
subscribe: (NostrFilter, String, (NostrEvent) -> Unit) -> String,
unsubscribe: (String) -> Unit
) {
this.relayLookup = relayManager
this.subscribeFunc = subscribe
this.unsubscribeFunc = unsubscribe
}
/**
* Subscribe to count notes for a specific geohash
* iOS: Validates building-level precision (8 chars) and checks for relay availability
*/
fun subscribe(geohash: String) {
val normalized = geohash.lowercase()
// Skip if already subscribed to this geohash
if (_geohash.value == normalized && subscriptionID != null) {
Log.d(TAG, "Already subscribed to geohash: $normalized")
return
}
// Validate geohash (building-level precision: 8 chars) - matches iOS
if (!isValidBuildingGeohash(normalized)) {
Log.w(TAG, "LocationNotesCounter: rejecting invalid geohash '$normalized' (expected 8 valid base32 chars)")
return
}
Log.d(TAG, "Subscribing to count notes for geohash: $normalized")
// Unsubscribe previous without clearing count to avoid flicker (iOS pattern)
subscriptionID?.let { subId ->
unsubscribeFunc?.invoke(subId)
}
subscriptionID = null
// Reset state
_geohash.value = normalized
eventIDs.clear()
_initialLoadComplete.value = false
_relayAvailable.value = true
// Get geo-specific relays (iOS pattern: dependencies.relayLookup(norm, TransportConfig.nostrGeoRelayCount))
val relays = try {
com.bitchat.android.nostr.RelayDirectory.closestRelaysForGeohash(normalized, 5)
} catch (e: Exception) {
Log.e(TAG, "Failed to lookup relays for geohash $normalized: ${e.message}")
emptyList()
}
// Check if we have relays (iOS pattern: guard !relays.isEmpty())
if (relays.isEmpty()) {
Log.w(TAG, "LocationNotesCounter: no geo relays for geohash=$normalized")
_relayAvailable.value = false
_initialLoadComplete.value = true
_count.value = 0
return
}
val subscribe = subscribeFunc
if (subscribe == null) {
Log.e(TAG, "Cannot subscribe - subscribe function not initialized")
return
}
val filter = NostrFilter.geohashNotes(
geohash = normalized,
since = null,
limit = 200 // Count up to 200 recent notes
)
// iOS format: "locnotes-count-\(norm)-\(UUID().uuidString.prefix(6))"
val subId = "locnotes-count-$normalized-${java.util.UUID.randomUUID().toString().take(6)}"
subscriptionID = subscribe(filter, subId) { event ->
handleEvent(event, normalized)
}
// Mark initial load complete after brief delay
kotlinx.coroutines.MainScope().launch {
kotlinx.coroutines.delay(1500)
if (!_initialLoadComplete.value!!) {
_initialLoadComplete.value = true
Log.d(TAG, "Initial count load complete: ${_count.value} notes")
}
}
}
/**
* Validate building-level geohash (precision 8) - matches iOS Geohash.isValidBuildingGeohash
*/
private fun isValidBuildingGeohash(geohash: String): Boolean {
if (geohash.length != 8) return false
val base32Chars = "0123456789bcdefghjkmnpqrstuvwxyz"
return geohash.all { it in base32Chars }
}
/**
* Handle incoming event
*/
private fun handleEvent(event: NostrEvent, expectedGeohash: String) {
// Validate event
if (event.kind != NostrKind.TEXT_NOTE) {
return
}
// Check for geohash tag
val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }
if (geohashTag == null) {
return
}
// Check if matches expected geohash
val eventGeohash = geohashTag[1]
if (eventGeohash != expectedGeohash) {
return
}
// Deduplicate and count
if (!eventIDs.contains(event.id)) {
eventIDs.add(event.id)
_count.value = eventIDs.size
if (!_initialLoadComplete.value!!) {
_initialLoadComplete.value = true
}
}
}
/**
* Cancel subscription
*/
fun cancel() {
subscriptionID?.let { subId ->
Log.d(TAG, "🚫 Canceling counter subscription: $subId")
unsubscribeFunc?.invoke(subId)
subscriptionID = null
}
_geohash.value = null
_count.value = 0
eventIDs.clear()
_initialLoadComplete.value = false
}
}
@@ -29,6 +29,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.core.ui.utils.singleOrTripleClickable import com.bitchat.android.core.ui.utils.singleOrTripleClickable
import com.bitchat.android.geohash.LocationChannelManager.PermissionState import com.bitchat.android.geohash.LocationChannelManager.PermissionState
import androidx.compose.foundation.Canvas
import androidx.compose.ui.geometry.Offset
/** /**
* Header components for ChatScreen * Header components for ChatScreen
@@ -54,23 +56,27 @@ fun isFavoriteReactive(
} }
@Composable @Composable
fun TorStatusIcon( fun TorStatusDot(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val torStatus by com.bitchat.android.net.TorManager.statusFlow.collectAsState() val torStatus by com.bitchat.android.net.TorManager.statusFlow.collectAsState()
if (torStatus.mode != com.bitchat.android.net.TorMode.OFF) { if (torStatus.mode != com.bitchat.android.net.TorMode.OFF) {
val cableColor = when { val dotColor = when {
torStatus.running && torStatus.bootstrapPercent < 100 -> Color(0xFFFF9500) torStatus.running && torStatus.bootstrapPercent < 100 -> Color(0xFFFF9500) // Orange - bootstrapping
torStatus.running && torStatus.bootstrapPercent >= 100 -> Color(0xFF00C851) torStatus.running && torStatus.bootstrapPercent >= 100 -> Color(0xFF00C851) // Green - connected
else -> Color.Red else -> Color.Red // Red - error/disconnected
}
Canvas(
modifier = modifier
) {
val radius = size.minDimension / 2
drawCircle(
color = dotColor,
radius = radius,
center = Offset(size.width / 2, size.height / 2)
)
} }
Icon(
imageVector = Icons.Outlined.Cable,
contentDescription = stringResource(R.string.cd_tor_status),
modifier = modifier,
tint = cableColor
)
} }
} }
@@ -156,6 +162,10 @@ fun NicknameEditor(
.widthIn(max = 120.dp) .widthIn(max = 120.dp)
.horizontalScroll(scrollState) .horizontalScroll(scrollState)
) )
// Tor status dot - constant distance from nickname end
Spacer(modifier = Modifier.width(6.dp))
TorStatusDot(modifier = Modifier.size(6.dp))
} }
} }
@@ -530,9 +540,10 @@ private fun MainHeader(
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.observeAsState(emptyList())
// Location notes counter (iOS parity) // SIMPLIFIED: Get notes count directly from LocationNotesManager (no separate counter)
val notesCounter = remember { com.bitchat.android.nostr.LocationNotesCounter } val notesManager = remember { com.bitchat.android.nostr.LocationNotesManager.getInstance() }
val notesCount by notesCounter.count.observeAsState(0) val notes by notesManager.notes.observeAsState(emptyList())
val notesCount = notes.size
// Location channel manager for permission state // Location channel manager for permission state
val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) } val locationManager = remember { com.bitchat.android.geohash.LocationChannelManager.getInstance(context) }
@@ -632,9 +643,6 @@ private fun MainHeader(
} }
} }
} }
// Tor status cable icon when Tor is enabled
TorStatusIcon(modifier = Modifier.size(14.dp))
// PoW status indicator // PoW status indicator
PoWStatusIndicator( PoWStatusIndicator(
@@ -148,8 +148,8 @@ class ChatViewModel(
} }
} }
// Initialize location notes counter subscription management (iOS parity) // SIMPLIFIED: Initialize location notes manager subscription (no separate counter)
initializeLocationNotesCounterSubscription() initializeLocationNotesManagerSubscription()
} }
fun cancelMediaSend(messageId: String) { fun cancelMediaSend(messageId: String) {
@@ -595,24 +595,23 @@ class ChatViewModel(
} }
/** /**
* Initialize location notes counter subscription management (iOS parity) * SIMPLIFIED: Initialize location notes manager subscription (no separate counter)
* Subscribes/unsubscribes based on channel selection and location permission * Subscribes/unsubscribes based on channel selection and location permission
*/ */
private fun initializeLocationNotesCounterSubscription() { private fun initializeLocationNotesManagerSubscription() {
viewModelScope.launch { viewModelScope.launch {
// Observe channel changes // Observe channel changes
selectedLocationChannel.observeForever { channel -> selectedLocationChannel.observeForever { channel ->
updateNotesCounterSubscription(channel) updateLocationNotesSubscription(channel)
} }
} }
} }
/** /**
* Update notes counter subscription based on current channel and location permission * SIMPLIFIED: Update location notes subscription based on current channel and location permission
* iOS pattern: Subscribe when in mesh mode and location is authorized * iOS pattern: Subscribe when in mesh mode and location is authorized
* ENHANCEMENT: Also subscribe LocationNotesManager to load actual notes in background
*/ */
private fun updateNotesCounterSubscription(channel: com.bitchat.android.geohash.ChannelID?) { private fun updateLocationNotesSubscription(channel: com.bitchat.android.geohash.ChannelID?) {
try { try {
val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication()) val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
val permissionState = locationManager.permissionState.value val permissionState = locationManager.permissionState.value
@@ -620,9 +619,9 @@ class ChatViewModel(
when (channel) { when (channel) {
is com.bitchat.android.geohash.ChannelID.Mesh -> { is com.bitchat.android.geohash.ChannelID.Mesh -> {
// Mesh mode: subscribe to block-level geohash notes if location authorized // Mesh mode: subscribe to building-level geohash notes if location authorized
if (locationPermissionGranted) { if (locationPermissionGranted) {
// Refresh location to get current block geohash // Refresh location to get current building geohash
locationManager.refreshChannels() locationManager.refreshChannels()
// Get building-level geohash (precision 8, same as iOS) // Get building-level geohash (precision 8, same as iOS)
@@ -632,25 +631,19 @@ class ChatViewModel(
if (buildingGeohash != null) { if (buildingGeohash != null) {
Log.d(TAG, "📍 Subscribing to location notes for building geohash: $buildingGeohash") Log.d(TAG, "📍 Subscribing to location notes for building geohash: $buildingGeohash")
// Subscribe counter for badge
com.bitchat.android.nostr.LocationNotesCounter.subscribe(buildingGeohash)
// ALSO subscribe manager to load actual notes in background
com.bitchat.android.nostr.LocationNotesManager.getInstance().setGeohash(buildingGeohash) com.bitchat.android.nostr.LocationNotesManager.getInstance().setGeohash(buildingGeohash)
} else { } else {
// Keep existing subscription if we had one (avoid flicker) // Cancel if no building geohash available
if (com.bitchat.android.nostr.LocationNotesCounter.geohash.value == null) { if (com.bitchat.android.nostr.LocationNotesManager.getInstance().geohash.value == null) {
com.bitchat.android.nostr.LocationNotesCounter.cancel()
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel() com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
} }
} }
} else { } else {
com.bitchat.android.nostr.LocationNotesCounter.cancel()
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel() com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
} }
} }
is com.bitchat.android.geohash.ChannelID.Location -> { is com.bitchat.android.geohash.ChannelID.Location -> {
// Location channel mode: cancel counter (only show in mesh mode) // Location channel mode: cancel (only show in mesh mode)
com.bitchat.android.nostr.LocationNotesCounter.cancel()
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel() com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
} }
null -> { null -> {
@@ -661,7 +654,7 @@ class ChatViewModel(
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "updateNotesCounterSubscription failed: ${e.message}") Log.w(TAG, "updateLocationNotesSubscription failed: ${e.message}")
} }
} }
@@ -24,7 +24,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
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.LocationNotesCounter
import com.bitchat.android.nostr.LocationNotesManager import com.bitchat.android.nostr.LocationNotesManager
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -52,16 +51,17 @@ fun LocationNotesSheet(
// Managers // Managers
val notesManager = remember { LocationNotesManager.getInstance() } val notesManager = remember { LocationNotesManager.getInstance() }
val counter = remember { LocationNotesCounter }
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.observeAsState(emptyList())
val state by notesManager.state.observeAsState(LocationNotesManager.State.IDLE) val state by notesManager.state.observeAsState(LocationNotesManager.State.IDLE)
val errorMessage by notesManager.errorMessage.observeAsState() val errorMessage by notesManager.errorMessage.observeAsState()
val count by counter.count.observeAsState(0)
val initialLoadComplete by notesManager.initialLoadComplete.observeAsState(false) val initialLoadComplete by notesManager.initialLoadComplete.observeAsState(false)
// SIMPLIFIED: Get count directly from notes list (no separate counter needed)
val count = notes.size
// Get location name (building or block) - matches iOS locationNames lookup // Get location name (building or block) - matches iOS locationNames lookup
val locationNames by locationManager.locationNames.observeAsState(emptyMap()) val locationNames by locationManager.locationNames.observeAsState(emptyMap())
val displayLocationName = locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() } val displayLocationName = locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() }
@@ -77,7 +77,6 @@ fun LocationNotesSheet(
// Effect to set geohash when sheet opens // Effect to set geohash when sheet opens
LaunchedEffect(geohash) { LaunchedEffect(geohash) {
notesManager.setGeohash(geohash) notesManager.setGeohash(geohash)
counter.subscribe(geohash)
} }
// Cleanup when sheet closes // Cleanup when sheet closes