mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 05:05:20 +00:00
simplify and tor icon change
This commit is contained in:
@@ -598,36 +598,7 @@ class MainActivity : ComponentActivity() {
|
||||
PoWPreferenceManager.init(this@MainActivity)
|
||||
Log.d("MainActivity", "PoW preferences initialized")
|
||||
|
||||
// Initialize Location Notes Counter (iOS parity)
|
||||
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)
|
||||
// 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 ->
|
||||
|
||||
@@ -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 com.bitchat.android.core.ui.utils.singleOrTripleClickable
|
||||
import com.bitchat.android.geohash.LocationChannelManager.PermissionState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
||||
/**
|
||||
* Header components for ChatScreen
|
||||
@@ -54,23 +56,27 @@ fun isFavoriteReactive(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TorStatusIcon(
|
||||
fun TorStatusDot(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val torStatus by com.bitchat.android.net.TorManager.statusFlow.collectAsState()
|
||||
|
||||
if (torStatus.mode != com.bitchat.android.net.TorMode.OFF) {
|
||||
val cableColor = when {
|
||||
torStatus.running && torStatus.bootstrapPercent < 100 -> Color(0xFFFF9500)
|
||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> Color(0xFF00C851)
|
||||
else -> Color.Red
|
||||
val dotColor = when {
|
||||
torStatus.running && torStatus.bootstrapPercent < 100 -> Color(0xFFFF9500) // Orange - bootstrapping
|
||||
torStatus.running && torStatus.bootstrapPercent >= 100 -> Color(0xFF00C851) // Green - connected
|
||||
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)
|
||||
.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 bookmarks by bookmarksStore.bookmarks.observeAsState(emptyList())
|
||||
|
||||
// Location notes counter (iOS parity)
|
||||
val notesCounter = remember { com.bitchat.android.nostr.LocationNotesCounter }
|
||||
val notesCount by notesCounter.count.observeAsState(0)
|
||||
// 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) }
|
||||
@@ -632,9 +643,6 @@ private fun MainHeader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tor status cable icon when Tor is enabled
|
||||
TorStatusIcon(modifier = Modifier.size(14.dp))
|
||||
|
||||
// PoW status indicator
|
||||
PoWStatusIndicator(
|
||||
|
||||
@@ -148,8 +148,8 @@ class ChatViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize location notes counter subscription management (iOS parity)
|
||||
initializeLocationNotesCounterSubscription()
|
||||
// SIMPLIFIED: Initialize location notes manager subscription (no separate counter)
|
||||
initializeLocationNotesManagerSubscription()
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
private fun initializeLocationNotesCounterSubscription() {
|
||||
private fun initializeLocationNotesManagerSubscription() {
|
||||
viewModelScope.launch {
|
||||
// Observe channel changes
|
||||
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
|
||||
* 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 {
|
||||
val locationManager = com.bitchat.android.geohash.LocationChannelManager.getInstance(getApplication())
|
||||
val permissionState = locationManager.permissionState.value
|
||||
@@ -620,9 +619,9 @@ class ChatViewModel(
|
||||
|
||||
when (channel) {
|
||||
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) {
|
||||
// Refresh location to get current block geohash
|
||||
// Refresh location to get current building geohash
|
||||
locationManager.refreshChannels()
|
||||
|
||||
// Get building-level geohash (precision 8, same as iOS)
|
||||
@@ -632,25 +631,19 @@ class ChatViewModel(
|
||||
|
||||
if (buildingGeohash != null) {
|
||||
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)
|
||||
} else {
|
||||
// Keep existing subscription if we had one (avoid flicker)
|
||||
if (com.bitchat.android.nostr.LocationNotesCounter.geohash.value == null) {
|
||||
com.bitchat.android.nostr.LocationNotesCounter.cancel()
|
||||
// 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.LocationNotesCounter.cancel()
|
||||
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
|
||||
}
|
||||
}
|
||||
is com.bitchat.android.geohash.ChannelID.Location -> {
|
||||
// Location channel mode: cancel counter (only show in mesh mode)
|
||||
com.bitchat.android.nostr.LocationNotesCounter.cancel()
|
||||
// Location channel mode: cancel (only show in mesh mode)
|
||||
com.bitchat.android.nostr.LocationNotesManager.getInstance().cancel()
|
||||
}
|
||||
null -> {
|
||||
@@ -661,7 +654,7 @@ class ChatViewModel(
|
||||
}
|
||||
}
|
||||
} 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 com.bitchat.android.geohash.GeohashChannelLevel
|
||||
import com.bitchat.android.geohash.LocationChannelManager
|
||||
import com.bitchat.android.nostr.LocationNotesCounter
|
||||
import com.bitchat.android.nostr.LocationNotesManager
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -52,16 +51,17 @@ fun LocationNotesSheet(
|
||||
|
||||
// Managers
|
||||
val notesManager = remember { LocationNotesManager.getInstance() }
|
||||
val counter = remember { LocationNotesCounter }
|
||||
val locationManager = remember { LocationChannelManager.getInstance(context) }
|
||||
|
||||
// State
|
||||
val notes by notesManager.notes.observeAsState(emptyList())
|
||||
val state by notesManager.state.observeAsState(LocationNotesManager.State.IDLE)
|
||||
val errorMessage by notesManager.errorMessage.observeAsState()
|
||||
val count by counter.count.observeAsState(0)
|
||||
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
|
||||
val locationNames by locationManager.locationNames.observeAsState(emptyMap())
|
||||
val displayLocationName = locationNames[GeohashChannelLevel.BUILDING]?.takeIf { it.isNotEmpty() }
|
||||
@@ -77,7 +77,6 @@ fun LocationNotesSheet(
|
||||
// Effect to set geohash when sheet opens
|
||||
LaunchedEffect(geohash) {
|
||||
notesManager.setGeohash(geohash)
|
||||
counter.subscribe(geohash)
|
||||
}
|
||||
|
||||
// Cleanup when sheet closes
|
||||
|
||||
Reference in New Issue
Block a user