From 9518d386d0f6ab3859a7373cb98a16f18fb934b5 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Sat, 6 Sep 2025 10:41:51 +0200
Subject: [PATCH 1/9] better tor management (#380)
* better tor management
* better tracking
---
app/src/main/java/com/bitchat/android/net/TorManager.kt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/com/bitchat/android/net/TorManager.kt b/app/src/main/java/com/bitchat/android/net/TorManager.kt
index e04c2240..efa2eab5 100644
--- a/app/src/main/java/com/bitchat/android/net/TorManager.kt
+++ b/app/src/main/java/com/bitchat/android/net/TorManager.kt
@@ -344,7 +344,8 @@ object TorManager {
bindRetryAttempts = 0
startInactivityMonitoring()
}
- s.contains("AMEx: state changed to Running", ignoreCase = true) -> {
+ //s.contains("AMEx: state changed to Running", ignoreCase = true) -> {
+ s.contains("We have found that guard [scrubbed] is usable.", ignoreCase = true) -> {
// If we already saw Sufficiently bootstrapped, mark as RUNNING and ready.
val bp = if (_status.value.bootstrapPercent >= 100) 100 else 100 // treat Running as ready
_status.value = _status.value.copy(state = TorState.RUNNING, bootstrapPercent = bp, running = true)
From cc45f477fbfde0bf9bf2958adf458e665a6f5e81 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Sat, 6 Sep 2025 10:58:39 +0200
Subject: [PATCH 2/9] init noise handshake on queue DMs (#379)
* init noise handshake on queue DMs
* establish noise more aggressively
---
.../bitchat/android/services/MessageRouter.kt | 2 +
.../bitchat/android/ui/PrivateChatManager.kt | 79 +------------------
2 files changed, 4 insertions(+), 77 deletions(-)
diff --git a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt
index bdf3e126..b968aa96 100644
--- a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt
+++ b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt
@@ -67,6 +67,8 @@ class MessageRouter private constructor(
Log.d(TAG, "Queued PM for ${toPeerID.take(8)}… (no mesh, no Nostr mapping) id=${messageID.take(8)}…")
val q = outbox.getOrPut(toPeerID) { mutableListOf() }
q.add(Triple(content, recipientNickname, messageID))
+ Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…")
+ mesh.initiateNoiseHandshake(toPeerID)
}
}
diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt
index cf549d76..3d849ac1 100644
--- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt
+++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt
@@ -365,87 +365,12 @@ class PrivateChatManager(
"Our peer ID lexicographically >= target peer ID, sending identity announcement to prompt handshake from $peerID"
)
meshService.sendAnnouncementToPeer(peerID)
+ Log.d(TAG, "Sent identity announcement to $peerID – starting handshake now from our side")
+ noiseSessionDelegate.initiateHandshake(peerID)
}
}
-// /**
-// * Legacy reflection-based implementation for backward compatibility
-// */
-// private fun establishNoiseSessionIfNeededLegacy(peerID: String, meshService: Any) {
-// try {
-// // Check if we already have an established Noise session with this peer
-// val hasSessionMethod = meshService::class.java.getDeclaredMethod("hasEstablishedSession", String::class.java)
-// val hasSession = hasSessionMethod.invoke(meshService, peerID) as Boolean
-//
-// if (hasSession) {
-// Log.d(TAG, "Noise session already established with $peerID")
-// return
-// }
-//
-// Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
-//
-// // Get our peer ID from mesh service for lexicographical comparison
-// val myPeerIDField = meshService::class.java.getField("myPeerID")
-// val myPeerID = myPeerIDField.get(meshService) as String
-//
-// // Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
-// if (myPeerID < peerID) {
-// // We should initiate the handshake
-// Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID")
-// initiateHandshakeWithPeer(peerID, meshService)
-// } else {
-// // They should initiate, we send a Noise identity announcement
-// Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
-// sendNoiseIdentityAnnouncement(meshService)
-// }
-//
-// } catch (e: Exception) {
-// Log.e(TAG, "Failed to establish Noise session with $peerID: ${e.message}")
-// }
-// }
-
- /**
- * Initiate handshake with specific peer using the existing delegate pattern
- */
- private fun initiateHandshakeWithPeer(peerID: String, meshService: Any) {
- try {
- // Use the existing MessageHandler delegate approach to initiate handshake
- // This calls the same code that's in MessageHandler's delegate.initiateNoiseHandshake()
- val messageHandler = meshService::class.java.getDeclaredField("messageHandler")
- messageHandler.isAccessible = true
- val handler = messageHandler.get(meshService)
-
- val delegate = handler::class.java.getDeclaredField("delegate")
- delegate.isAccessible = true
- val handlerDelegate = delegate.get(handler)
-
- val method =
- handlerDelegate::class.java.getMethod("initiateNoiseHandshake", String::class.java)
- method.invoke(handlerDelegate, peerID)
-
- Log.d(TAG, "Successfully initiated Noise handshake with $peerID using delegate pattern")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}")
- }
- }
-
- /**
- * Send Noise identity announcement to prompt other peer to initiate handshake
- * This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService
- */
- private fun sendNoiseIdentityAnnouncement(meshService: Any) {
- try {
- // Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement
- val method =
- meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement")
- method.invoke(meshService)
- Log.d(TAG, "Successfully sent Noise identity announcement")
- } catch (e: Exception) {
- Log.e(TAG, "Failed to send Noise identity announcement: ${e.message}")
- }
- }
-
// MARK: - Utility Functions
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
From a804782476c922cdacb7e7c3defbecfe7ce01014 Mon Sep 17 00:00:00 2001
From: lollerfirst <43107113+lollerfirst@users.noreply.github.com>
Date: Sat, 6 Sep 2025 11:49:23 +0200
Subject: [PATCH 3/9] feat(geohash): add in-app Geohash Picker (#363)
* feat(geohash): add in-app Geohash Picker map with quadrant drill-down and Activity integration
- New GeohashPickerActivity hosting a WebView with Leaflet-based picker
- Adds map icon next to custom geohash input in LocationChannelsSheet
- Picker allows quadrant selection and subquadrant drill-down; returns selected geohash
- Register activity in AndroidManifest
perf(picker): improve map performance and correctness
- Use LayerGroup and clearLayers() on redraw to avoid stale overlays
- Use Leaflet canvas renderer for rectangles to reduce DOM and improve perf
- Make labels non-interactive and pointer-events: none to avoid gesture overhead
- Fix initial mega-label artifact by grouping and clearing labels with cells
chore: add asset geohash_picker.html with minimal geohash helpers (bounds/encode/adjacent)
* remove accidentally committed submodule.
* fix some issues
* better geohash labels on map
* design wip
* countries green
* wip
* geohashpicker: pan + zoom; start from current geohash channel
* better ui
* ui elements
* nice
* readability
---------
Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
---
app/src/main/AndroidManifest.xml | 6 +
app/src/main/assets/geohash_picker.html | 225 ++++++++++++++
.../android/ui/GeohashPickerActivity.kt | 283 ++++++++++++++++++
.../android/ui/LocationChannelsSheet.kt | 35 +++
4 files changed, 549 insertions(+)
create mode 100644 app/src/main/assets/geohash_picker.html
create mode 100644 app/src/main/java/com/bitchat/android/ui/GeohashPickerActivity.kt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2b61df2f..9516695d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,6 +41,12 @@
android:supportsRtl="true"
android:theme="@style/Theme.BitchatAndroid"
tools:targetApi="31">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/bitchat/android/ui/GeohashPickerActivity.kt b/app/src/main/java/com/bitchat/android/ui/GeohashPickerActivity.kt
new file mode 100644
index 00000000..97f1d3ce
--- /dev/null
+++ b/app/src/main/java/com/bitchat/android/ui/GeohashPickerActivity.kt
@@ -0,0 +1,283 @@
+package com.bitchat.android.ui
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Bundle
+import android.view.ViewGroup
+import android.webkit.JavascriptInterface
+import android.webkit.WebChromeClient
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.view.updateLayoutParams
+import com.bitchat.android.geohash.Geohash
+import com.bitchat.android.geohash.LocationChannelManager
+import com.bitchat.android.ui.theme.BASE_FONT_SIZE
+
+@OptIn(ExperimentalMaterial3Api::class)
+class GeohashPickerActivity : ComponentActivity() {
+
+ companion object {
+ const val EXTRA_INITIAL_GEOHASH = "initial_geohash"
+ const val EXTRA_RESULT_GEOHASH = "result_geohash"
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val initialGeohash = intent.getStringExtra(EXTRA_INITIAL_GEOHASH)?.trim()?.lowercase()
+ var geohashToFocus: String? = null
+ var (initLat, initLon) = 0.0 to 0.0
+
+ if (!initialGeohash.isNullOrEmpty()) {
+ geohashToFocus = initialGeohash
+ try {
+ val (lat, lon) = Geohash.decodeToCenter(initialGeohash)
+ initLat = lat
+ initLon = lon
+ } catch (_: Throwable) {}
+ } else {
+ // If no initial geohash, try to use the user's coarsest location
+ val locationManager = LocationChannelManager.getInstance(applicationContext)
+ val channels = locationManager.availableChannels.value
+ if (!channels.isNullOrEmpty()) {
+ val coarsestChannel = channels.minByOrNull { it.geohash.length }
+ if (coarsestChannel != null) {
+ geohashToFocus = coarsestChannel.geohash
+ try {
+ val (lat, lon) = Geohash.decodeToCenter(coarsestChannel.geohash)
+ initLat = lat
+ initLon = lon
+ } catch (_: Throwable) {}
+ }
+ }
+ }
+
+ val initialPrecision = geohashToFocus?.length ?: 5
+
+ setContent {
+ MaterialTheme {
+ var currentGeohash by remember { mutableStateOf(geohashToFocus ?: "") }
+ var precision by remember { mutableStateOf(initialPrecision.coerceIn(1, 12)) }
+ var webViewRef by remember { mutableStateOf(null) }
+
+ // iOS system-like colors used across app
+ val colorScheme = MaterialTheme.colorScheme
+ val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
+ val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
+
+ Scaffold { padding ->
+ Box(Modifier.fillMaxSize()) {
+ AndroidView(
+ factory = { context ->
+ WebView(context).apply {
+ settings.javaScriptEnabled = true
+ settings.domStorageEnabled = true
+ settings.cacheMode = WebSettings.LOAD_DEFAULT
+ settings.allowFileAccess = true
+ settings.allowContentAccess = true
+ webChromeClient = WebChromeClient()
+ webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ super.onPageFinished(view, url)
+ // Initialize to last/initial geohash if provided, otherwise center
+ if (!geohashToFocus.isNullOrEmpty()) {
+ evaluateJavascript(
+ "window.focusGeohash('${geohashToFocus}')",
+ null
+ )
+ } else {
+ evaluateJavascript(
+ "window.setCenter(${initLat}, ${initLon})",
+ null
+ )
+ }
+
+ // Apply theme to map tiles
+ val nightModeFlags = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ val theme = if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) "dark" else "light"
+ evaluateJavascript("window.setMapTheme('" + theme + "')", null)
+ }
+ }
+ addJavascriptInterface(object {
+ @JavascriptInterface
+ fun onGeohashChanged(geohash: String) {
+ runOnUiThread {
+ currentGeohash = geohash
+ }
+ }
+ }, "Android")
+
+ loadUrl("file:///android_asset/geohash_picker.html")
+ }
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ update = { webView ->
+ webViewRef = webView
+ // ensure it fills parent
+ webView.updateLayoutParams {
+ width = ViewGroup.LayoutParams.MATCH_PARENT
+ height = ViewGroup.LayoutParams.MATCH_PARENT
+ }
+ },
+ onRelease = { webView ->
+ // Best-effort cleanup to avoid leaks and timers
+ try { webView.evaluateJavascript("window.cleanup && window.cleanup()", null) } catch (_: Throwable) {}
+ try { webView.stopLoading() } catch (_: Throwable) {}
+ try { webView.clearHistory() } catch (_: Throwable) {}
+ try { webView.clearCache(true) } catch (_: Throwable) {}
+ try { webView.loadUrl("about:blank") } catch (_: Throwable) {}
+ try { webView.removeAllViews() } catch (_: Throwable) {}
+ try { webView.destroy() } catch (_: Throwable) {}
+ }
+ )
+
+ // Floating info pill
+ Surface(
+ modifier = Modifier
+ .align(Alignment.TopCenter)
+ .padding(top = 20.dp)
+ .fillMaxWidth(0.75f),
+ color = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f),
+ shape = RoundedCornerShape(12.dp),
+ tonalElevation = 3.dp,
+ shadowElevation = 6.dp
+ ) {
+ Text(
+ text = "pan and zoom to select a geohash",
+ fontSize = 12.sp,
+ textAlign = TextAlign.Center,
+ fontFamily = FontFamily.Monospace,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier
+ .padding(horizontal = 14.dp, vertical = 10.dp)
+ )
+ }
+
+ // Floating bottom controls
+ Column(
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 20.dp, start = 16.dp, end = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ // Geohash label (monospace, app style)
+ Surface(
+ color = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f),
+ shape = RoundedCornerShape(12.dp),
+ tonalElevation = 3.dp,
+ shadowElevation = 6.dp
+ ) {
+ Text(
+ text = if (currentGeohash.isNotEmpty()) "#${currentGeohash}" else "select location",
+ fontSize = BASE_FONT_SIZE.sp,
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier
+ .padding(horizontal = 14.dp, vertical = 10.dp)
+ )
+ }
+
+ // Button row
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Decrease precision
+ Button(
+ onClick = {
+ precision = (precision - 1).coerceAtLeast(1)
+ webViewRef?.evaluateJavascript("window.setPrecision($precision)", null)
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = standardGreen.copy(alpha = 0.12f),
+ contentColor = standardGreen
+ )
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Filled.Remove, contentDescription = "Decrease precision")
+ }
+ }
+
+ // Increase precision
+ Button(
+ onClick = {
+ precision = (precision + 1).coerceAtMost(12)
+ webViewRef?.evaluateJavascript("window.setPrecision($precision)", null)
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = standardGreen.copy(alpha = 0.12f),
+ contentColor = standardGreen
+ )
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Filled.Add, contentDescription = "Increase precision")
+ }
+ }
+
+ // Select button
+ Button(
+ onClick = {
+ webViewRef?.evaluateJavascript("window.getGeohash()") { value ->
+ val gh = value?.trim('"') ?: currentGeohash
+ val result = Intent().apply { putExtra(EXTRA_RESULT_GEOHASH, gh) }
+ setResult(Activity.RESULT_OK, result)
+ finish()
+ }
+ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
+ contentColor = MaterialTheme.colorScheme.onSurface
+ )
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Filled.Check, contentDescription = "Select geohash")
+ Spacer(Modifier.width(6.dp))
+ Text(
+ text = "select",
+ fontSize = (BASE_FONT_SIZE - 2).sp,
+ fontFamily = FontFamily.Monospace
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt b/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt
index 66a86ef1..e21dd8f5 100644
--- a/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt
+++ b/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt
@@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Map
import androidx.compose.material.icons.filled.PinDrop
import androidx.compose.material3.*
import androidx.compose.ui.text.font.FontWeight
@@ -30,6 +31,8 @@ import com.bitchat.android.geohash.GeohashChannel
import com.bitchat.android.geohash.GeohashChannelLevel
import com.bitchat.android.geohash.LocationChannelManager
import java.util.*
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
/**
* Location Channels Sheet for selecting geohash-based location channels
@@ -71,6 +74,18 @@ fun LocationChannelsSheet(
// Scroll state for LazyColumn
val listState = rememberLazyListState()
+ val mapPickerLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == android.app.Activity.RESULT_OK) {
+ val gh = result.data?.getStringExtra(GeohashPickerActivity.EXTRA_RESULT_GEOHASH)
+ if (!gh.isNullOrBlank()) {
+ customGeohash = gh
+ customError = null
+ }
+ }
+ }
+
// iOS system colors (matches iOS exactly)
val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
@@ -312,6 +327,26 @@ fun LocationChannelsSheet(
)
val normalized = customGeohash.trim().lowercase().replace("#", "")
+
+ // Map picker button
+ IconButton(onClick = {
+ val initial = when {
+ normalized.isNotBlank() -> normalized
+ selectedChannel is ChannelID.Location -> (selectedChannel as ChannelID.Location).channel.geohash
+ else -> ""
+ }
+ val intent = Intent(context, GeohashPickerActivity::class.java).apply {
+ putExtra(GeohashPickerActivity.EXTRA_INITIAL_GEOHASH, initial)
+ }
+ mapPickerLauncher.launch(intent)
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Map,
+ contentDescription = "Open map",
+ tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
+ )
+ }
+
val isValid = validateGeohash(normalized)
// iOS-style teleport button
From b131554efe4689c2c11a15064bb5f5bb1142eaf4 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:45:48 +0200
Subject: [PATCH 4/9] fix some debug settings (#383)
---
.../mesh/BluetoothConnectionTracker.kt | 1 -
.../android/ui/debug/DebugSettingsSheet.kt | 64 ++++++++++++++-----
2 files changed, 48 insertions(+), 17 deletions(-)
diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt
index 3494d1b2..f1c9009b 100644
--- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt
+++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt
@@ -277,7 +277,6 @@ class BluetoothConnectionTracker(
fun cleanupDeviceConnection(deviceAddress: String) {
connectedDevices.remove(deviceAddress)?.let { deviceConn ->
subscribedDevices.removeAll { it.address == deviceAddress }
- addressPeerMap.remove(deviceAddress)
}
pendingConnections.remove(deviceAddress)
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt
index 33fde064..03453aaf 100644
--- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt
+++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt
@@ -172,6 +172,8 @@ fun DebugSettingsSheet(
steps = 30
)
}
+ val overallCount = connectedDevices.size
+ Text("connections: $overallCount / $maxOverall", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
Row(verticalAlignment = Alignment.CenterVertically) {
Text("max overall", fontFamily = FontFamily.Monospace, modifier = Modifier.width(90.dp))
Slider(
@@ -217,36 +219,66 @@ fun DebugSettingsSheet(
}
val maxValRaw = series.maxOrNull() ?: 0f
val maxVal = if (maxValRaw > 0f) maxValRaw else 0f
- Box(Modifier.fillMaxWidth().height(48.dp)) {
+ val leftGutter = 40.dp
+ Box(Modifier.fillMaxWidth().height(56.dp)) {
+ // Graph canvas
androidx.compose.foundation.Canvas(Modifier.fillMaxSize()) {
- val axisPx = 28.dp.toPx() // leave room on left for ticks
+ val axisPx = leftGutter.toPx() // reserved left gutter for labels
val barCount = series.size
val availW = (size.width - axisPx).coerceAtLeast(1f)
val w = availW / barCount
val h = size.height
- // draw baseline at y=0 (bottom)
+ // Baseline at bottom (y = 0)
drawLine(
color = Color(0x33888888),
start = androidx.compose.ui.geometry.Offset(axisPx, h - 1f),
end = androidx.compose.ui.geometry.Offset(size.width, h - 1f),
strokeWidth = 1f
)
+ // Bars from bottom-up; skip zeros entirely
series.forEachIndexed { i, value ->
- val ratio = if (maxVal > 0f) (value / maxVal).coerceIn(0f, 1f) else 0f // min always 0
- val barHeight = h * ratio
- // Draw bars from bottom up, starting after left axis area
- drawRect(
- color = Color(0xFF00C851),
- topLeft = androidx.compose.ui.geometry.Offset(x = axisPx + i * w, y = h - barHeight),
- size = androidx.compose.ui.geometry.Size(w, barHeight)
- )
+ if (value > 0f && maxVal > 0f) {
+ val ratio = (value / maxVal).coerceIn(0f, 1f)
+ val barHeight = (h * ratio).coerceAtLeast(0f)
+ if (barHeight > 0.5f) {
+ drawRect(
+ color = Color(0xFF00C851),
+ topLeft = androidx.compose.ui.geometry.Offset(x = axisPx + i * w, y = h - barHeight),
+ size = androidx.compose.ui.geometry.Size(w, barHeight)
+ )
+ }
+ }
}
}
- // Y-axis ticks (min/max) in the left margin
- Text("0", fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.BottomStart).padding(start = 4.dp, bottom = 2.dp))
- Text("${maxVal.toInt()}", fontFamily = FontFamily.Monospace, fontSize = 10.sp, color = colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.TopStart).padding(start = 4.dp, top = 2.dp))
- // Y-axis unit label (vertical)
- Text("p/s", fontFamily = FontFamily.Monospace, fontSize = 9.sp, color = colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.align(Alignment.CenterStart).padding(start = 2.dp).rotate(-90f))
+ // Left gutter layout: unit + ticks neatly aligned
+ Row(Modifier.fillMaxSize()) {
+ Box(Modifier.width(leftGutter).fillMaxHeight()) {
+ // Unit label on the far left, centered vertically
+ Text(
+ "p/s",
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp,
+ color = colorScheme.onSurface.copy(alpha = 0.7f),
+ modifier = Modifier.align(Alignment.CenterStart).padding(start = 2.dp).rotate(-90f)
+ )
+ // Tick labels right-aligned in gutter, top and bottom aligned
+ Text(
+ "${maxVal.toInt()}",
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp,
+ color = colorScheme.onSurface.copy(alpha = 0.7f),
+ modifier = Modifier.align(Alignment.TopEnd).padding(end = 4.dp, top = 0.dp)
+ )
+ Text(
+ "0",
+ fontFamily = FontFamily.Monospace,
+ fontSize = 10.sp,
+ color = colorScheme.onSurface.copy(alpha = 0.7f),
+ modifier = Modifier.align(Alignment.BottomEnd).padding(end = 4.dp, bottom = 0.dp)
+ )
+ }
+ Spacer(Modifier.weight(1f))
+ }
}
}
}
From 9b8f98ec7c846fe327fbdcb2ccadde3d91d47716 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Sat, 6 Sep 2025 14:49:00 +0200
Subject: [PATCH 5/9] bump version 1.2.3 (#384)
---
app/build.gradle.kts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d3798025..a1a06b79 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -13,8 +13,8 @@ android {
applicationId = "com.bitchat.droid"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
- versionCode = 18
- versionName = "1.2.2"
+ versionCode = 19
+ versionName = "1.2.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
From c20e9defdec53fe9af030f19f2b0d2a7f6d5957f Mon Sep 17 00:00:00 2001
From: GitHub Action
Date: Sun, 7 Sep 2025 06:05:54 +0000
Subject: [PATCH 6/9] Automated update of relay data - Sun Sep 7 06:05:54 UTC
2025
---
app/src/main/assets/nostr_relays.csv | 520 +++++++++++++--------------
1 file changed, 255 insertions(+), 265 deletions(-)
diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv
index 47e6582c..293271d1 100644
--- a/app/src/main/assets/nostr_relays.csv
+++ b/app/src/main/assets/nostr_relays.csv
@@ -1,273 +1,263 @@
Relay URL,Latitude,Longitude
-relay.chorus.community,50.1109,8.68213
-relay-rpi.edufeed.org,49.4543,11.0746
-nostr.chaima.info,50.1109,8.68213
-relay.nostraddress.com,43.6532,-79.3832
-nostr.middling.mydns.jp,35.8099,140.12
-nostr.myshosholoza.co.za,52.3676,4.90414
-n.ok0.org,-37.7254,176.133
-relay.21e6.cz,50.1682,14.0546
-premium.primal.net,43.6532,-79.3832
-relay.stream.labs.h3.se,59.4016,17.9455
-nostr.night7.space,50.4754,12.3683
-nostr.4rs.nl,49.0291,8.35696
-relay.notoshi.win,13.7683,100.54
-nostr.carroarmato0.be,50.9017,3.52588
-relay.angor.io,48.1046,11.6002
-relay.nostriot.com,41.5695,-83.9786
-nostr-pub.wellorder.net,45.5201,-122.99
-relay04.lnfi.network,39.0997,-94.5786
-x.kojira.io,43.6532,-79.3832
-strfry.felixzieger.de,50.1013,8.62643
-purpura.cloud,43.6532,-79.3832
-alien.macneilmediagroup.com,43.6532,-79.3832
-wot.sudocarlos.com,51.5072,-0.127586
-relay02.lnfi.network,39.0997,-94.5786
-relay.getsafebox.app,43.6532,-79.3832
-wot.sebastix.social,51.8933,4.42083
-relay.dwadziesciajeden.pl,52.2297,21.0122
-nostr.roundrockbitcoiners.com,40.8054,-74.0241
-nostr.dlsouza.lol,50.1109,8.68213
-nostr-relay.shirogaku.xyz,43.6532,-79.3832
-yabu.me,35.6092,139.73
-nostr.stakey.net,52.3676,4.90414
-relay.sincensura.org,43.6532,-79.3832
-nostr-02.czas.top,51.2277,6.77346
-soloco.nl,43.6532,-79.3832
-nostream.breadslice.com,43.6532,-79.3832
-nostr.camalolo.com,24.1469,120.684
-nostr-dev.wellorder.net,45.5201,-122.99
-nostr.pleb.one,38.6327,-90.1961
-cyberspace.nostr1.com,40.7357,-74.1724
-relay.illuminodes.com,47.6061,-122.333
-satsage.xyz,37.3986,-121.964
-nostr.data.haus,50.4754,12.3683
-nostr.jfischer.org,49.0291,8.35696
-r.bitcoinhold.net,43.6532,-79.3832
-relay.barine.co,43.6532,-79.3832
-wot.sovbit.host,64.1466,-21.9426
-nostr.vulpem.com,49.4543,11.0746
-nostr.kalf.org,52.3676,4.90414
-nostr.thebiglake.org,32.71,-96.6745
-relay.bitcoinveneto.org,64.1466,-21.9426
-wot.dergigi.com,64.1476,-21.9392
-nos.xmark.cc,50.6924,3.20113
-nostr.88mph.life,43.6532,-79.3832
-relay.mess.ch,47.3591,8.55292
-nostr.thaliyal.com,40.8218,-74.45
-nostr.notribe.net,40.8302,-74.1299
-relay.orangepill.ovh,49.1689,-0.358841
-relay.artiostr.ch,43.6532,-79.3832
-relay.13room.space,43.6532,-79.3832
-relay.primal.net,43.6532,-79.3832
-strfry.shock.network,41.8959,-88.2169
-nostr.2b9t.xyz,34.0549,-118.243
-ribo.af.nostria.app,-26.2041,28.0473
-a.nos.lol,50.4754,12.3683
-relay.mccormick.cx,52.3563,4.95714
-relay.mostro.network,40.8302,-74.1299
-relay.bitcoinartclock.com,50.4754,12.3683
-nostr-relay.cbrx.io,43.6532,-79.3832
-prl.plus,55.7623,37.6381
-relay.endfiat.money,43.6532,-79.3832
-relay.hook.cafe,43.6532,-79.3832
-relay.siamdev.cc,13.9178,100.424
-wot.nostr.party,36.1627,-86.7816
-wot.basspistol.org,49.4521,11.0767
-dev-nostr.bityacht.io,25.0797,121.234
-orangesync.tech,51.5072,-0.127586
-nostr.casa21.space,43.6532,-79.3832
-relay.digitalezukunft.cyou,45.5019,-73.5674
-relay.nostrcheck.me,43.6532,-79.3832
-inbox.mycelium.social,38.627,-90.1994
-nostr.liberty.fans,36.9104,-89.5875
-nostr-verified.wellorder.net,45.5201,-122.99
-relay.sigit.io,50.4754,12.3683
-tollbooth.stens.dev,51.223,6.78245
-relay.nostromo.social,49.4543,11.0746
-wheat.happytavern.co,43.6532,-79.3832
-relay.vrtmrz.net,43.6532,-79.3832
-relayrs.notoshi.win,43.6532,-79.3832
-travis-shears-nostr-relay-v2.fly.dev,41.8781,-87.6298
-nostr.community.ath.cx,45.5029,-73.5723
-wot.girino.org,43.6532,-79.3832
-nostr.coincrowd.fund,39.0438,-77.4874
-relay.freeplace.nl,52.3676,4.90414
-nostr-03.dorafactory.org,1.35208,103.82
-relay5.bitransfer.org,43.6532,-79.3832
-nostr.tac.lol,47.4748,-122.273
-nostr.sagaciousd.com,49.2827,-123.121
-nostr.girino.org,43.6532,-79.3832
-nostr.jerrynya.fun,31.2304,121.474
-relay.agorist.space,52.3734,4.89406
-inbox.azzamo.net,52.2633,21.0283
-wot.codingarena.top,50.4754,12.3683
-relay.degmods.com,50.4754,12.3683
-articles.layer3.news,37.3387,-121.885
-nostr.rblb.it,43.4633,11.8796
-relay.davidebtc.me,51.5072,-0.127586
-relay.hodl.ar,-34.5904,-58.629
-nostr.rtvslawenia.com,49.4543,11.0746
-relay01.lnfi.network,39.0997,-94.5786
-strfry.bonsai.com,37.8715,-122.273
-relay.nostr.place,32.7767,-96.797
-relay.bitcoindistrict.org,43.6532,-79.3832
-relay.aloftus.io,34.0881,-118.379
-nostr.massmux.com,51.5072,-0.127586
-relay.wellorder.net,45.5201,-122.99
-santo.iguanatech.net,40.8302,-74.1299
relay.damus.io,43.6532,-79.3832
-relay.javi.space,43.4633,11.8796
-relay.nosto.re,51.8933,4.42083
-relay.utxo.farm,35.6916,139.768
-relay.fr13nd5.com,52.5233,13.3426
-librerelay.aaroniumii.com,43.6532,-79.3832
-relay.cypherflow.ai,48.8566,2.35222
-relay.letsfo.com,51.098,17.0321
-nostr.0x7e.xyz,47.4988,8.72369
-relay.bullishbounty.com,43.6532,-79.3832
-relay.tagayasu.xyz,43.6715,-79.38
-nostr.plantroon.com,50.1013,8.62643
-strfry.openhoofd.nl,51.9229,4.40833
-nostrelites.org,41.8781,-87.6298
-relay.credenso.cafe,43.1149,-80.7228
-relay.conduit.market,43.6532,-79.3832
-relay.olas.app,50.4754,12.3683
-relay.nostrdice.com,-33.8688,151.209
-nostr.oxtr.dev,50.4754,12.3683
-relay.nostr.band,60.1699,24.9384
-khatru.nostrver.se,51.8933,4.42083
-relay.coinos.io,43.6532,-79.3832
-nostr.l484.com,30.2944,-97.6223
-purplerelay.com,50.1109,8.68213
-relay.nostrhub.fr,48.1046,11.6002
-black.nostrcity.club,41.8781,-87.6298
-nostr.snowbla.de,60.1699,24.9384
-no.str.cr,9.93388,-84.0849
-nostr.rikmeijer.nl,50.4754,12.3683
-nproxy.kristapsk.lv,60.1699,24.9384
-wot.soundhsa.com,34.0479,-118.256
-nostr.kungfu-g.rip,33.7946,-84.4488
-nostr.spaceshell.xyz,43.6532,-79.3832
-nostr.mikoshi.de,50.1109,8.68213
-gnostr.com,40.9017,29.1616
-noxir.kpherox.dev,34.8587,135.509
-nostr.ser1.net,12.9716,77.5946
-nostr-relay.zimage.com,34.282,-118.439
nostr2.girino.org,43.6532,-79.3832
-dev-relay.lnfi.network,39.0997,-94.5786
-nostr.hifish.org,47.3769,8.54169
-itanostr.space,52.2931,4.79099
-srtrelay.c-stellar.net,43.6532,-79.3832
-wot.dtonon.com,43.6532,-79.3832
-relay.mwaters.net,50.9871,2.12554
-relay.nostr.net,50.4754,12.3683
-relay.nostx.io,43.6532,-79.3832
-relay.laantungir.net,-19.4692,-42.5315
-pyramid.fiatjaf.com,50.1109,8.68213
-nostr.einundzwanzig.space,50.1109,8.68213
-relay.satlantis.io,32.8769,-80.0114
-nostr-relay.amethyst.name,35.7327,-78.8503
-fanfares.nostr1.com,40.7057,-74.0136
-relay.evanverma.com,40.8302,-74.1299
-relay.wavlake.com,41.2619,-95.8608
-relay.bitchat.party,53.5501,-113.469
-nostr.yael.at,52.3563,4.95714
-fenrir-s.notoshi.win,43.6532,-79.3832
-relay.nostr.wirednet.jp,34.704,135.495
-relay.origin.land,35.6673,139.751
-relay.goodmorningbitcoin.com,43.6532,-79.3832
-relay.mattybs.lol,43.6532,-79.3832
-relay.nostr.vet,52.6467,4.7395
-relay.arx-ccn.com,50.4754,12.3683
-nostr.agentcampfire.com,50.8933,6.05805
-ribo.eu.nostria.app,52.3676,4.90414
-nostr.huszonegy.world,47.4979,19.0402
-nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
-temp.iris.to,43.6532,-79.3832
-relay.etch.social,41.2619,-95.8608
-relay.hasenpfeffr.com,39.0438,-77.4874
-relay.exit.pub,50.4754,12.3683
-relay.chakany.systems,43.6532,-79.3832
-nostr.coincards.com,45.4002,-75.8064
-nostr.hekster.org,37.3986,-121.964
-nostr-relay.psfoundation.info,39.0438,-77.4874
-nostr-2.21crypto.ch,47.1928,8.81494
-nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
-relay.toastr.net,40.8054,-74.0241
-relay.wolfcoil.com,35.6092,139.73
-nostr-02.dorafactory.org,1.35208,103.82
-portal-relay.pareto.space,49.4543,11.0746
-nostr.makibisskey.work,43.6532,-79.3832
-shu04.shugur.net,25.2604,55.2989
-relay.lnfi.network,39.0438,-77.4874
-nostr.smut.cloud,43.6532,-79.3832
-nostr.diakod.com,43.6532,-79.3832
-relay.lumina.rocks,49.0291,8.35695
-nostr.now,36.55,139.733
-relay.mockingyou.com,37.5485,-121.989
-nostr.blankfors.se,60.1699,24.9384
-adre.su,59.9311,30.3609
-nostr.tadryanom.me,43.6532,-79.3832
-relay2.angor.io,48.1046,11.6002
-shu02.shugur.net,21.4902,39.2246
-nostrelay.circum.space,51.2217,6.77616
-nostr.spacecitynode.com,29.7057,-95.2706
-vidono.apps.slidestr.net,48.8566,2.35222
-relay.btcwill.win,43.6532,-79.3832
-orangepiller.org,60.1699,24.9384
-solo.itsalldance.space,32.71,-96.6745
-relay.zone667.com,60.1699,24.9384
-nostr.azzamo.net,52.2633,21.0283
-internationalright-wing.org,-22.5986,-48.8003
-shu05.shugur.net,48.8566,2.35222
-relay.lifpay.me,1.35208,103.82
-slick.mjex.me,39.048,-77.4817
-theoutpost.life,64.1476,-21.9392
-nostr.rohoss.com,50.1109,8.68213
-relay.cosmicbolt.net,37.3986,-121.964
-relay.jeffg.fyi,43.6532,-79.3832
-nostr.satstralia.com,64.1476,-21.9392
-relay-admin.thaliyal.com,40.8218,-74.45
-social.proxymana.net,60.1699,24.9384
-nostr.sathoarder.com,48.5734,7.75211
-wot.nostr.place,30.2672,-97.7431
-nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832
-relay.nsnip.io,60.1699,24.9384
-nostr.zenon.network,43.5009,-70.4428
-relay.copylaradio.com,50.8198,-1.08798
-relay.nostrhub.tech,49.4543,11.0746
-ithurtswhenip.ee,51.0238,-1.33713
-nostr-01.yakihonne.com,1.32123,103.695
-nostr-relay.online,43.6532,-79.3832
-vitor.nostr1.com,40.7057,-74.0136
-relay-testnet.k8s.layer3.news,37.3387,-121.885
-schnorr.me,43.6532,-79.3832
-relay.artx.market,43.652,-79.3633
-nostr.21crypto.ch,47.1928,8.81494
-relayone.soundhsa.com,34.0479,-118.256
-relay.puresignal.news,43.6532,-79.3832
nostr.n7ekb.net,47.4941,-122.294
-wot.brightbolt.net,47.6735,-116.781
-nostr.overmind.lol,43.6532,-79.3832
-relay.magiccity.live,25.8128,-80.2377
-kitchen.zap.cooking,43.6532,-79.3832
-relay.satsdays.com,1.35208,103.82
-nostr-02.yakihonne.com,1.32123,103.695
-offchain.pub,36.1809,-115.241
-nostr.lojong.info,43.6532,-79.3832
-relay.tapestry.ninja,40.8054,-74.0241
-zap.watch,45.5029,-73.5723
-nostrelay.memory-art.xyz,43.6532,-79.3832
-nostr.spicyz.io,43.6532,-79.3832
-nostr.mom,50.4754,12.3683
-nos.lol,50.4754,12.3683
+rnostr.breadslice.com,43.6532,-79.3832
+portal-relay.pareto.space,49.4543,11.0746
+relay.hasenpfeffr.com,39.0438,-77.4874
+nostr.blankfors.se,60.1699,24.9384
+relay5.bitransfer.org,43.6532,-79.3832
+santo.iguanatech.net,40.8302,-74.1299
+nostr.ser1.net,12.9716,77.5946
+nostr.stakey.net,52.3676,4.90414
+relayrs.notoshi.win,43.6532,-79.3832
+orangepiller.org,60.1699,24.9384
+temp.iris.to,43.6532,-79.3832
+relay.cypherflow.ai,48.8566,2.35222
+nostr.kalf.org,52.3676,4.90414
+relay.unknown.cloud,43.6532,-79.3832
+relay.artx.market,43.652,-79.3633
+relay.arx-ccn.com,50.4754,12.3683
relay.electriclifestyle.com,26.2897,-80.1293
-wot.nostr.net,43.6532,-79.3832
-nostr.primz.org,43.6532,-79.3832
+nostr-relay.online,43.6532,-79.3832
+relay.jeffg.fyi,43.6532,-79.3832
+relay.wellorder.net,45.5201,-122.99
+relay01.lnfi.network,39.0997,-94.5786
+noxir.kpherox.dev,34.8587,135.509
+wot.sebastix.social,51.8933,4.42083
+nostr.overmind.lol,43.6532,-79.3832
nostr.red5d.dev,43.6532,-79.3832
-relay.0xchat.com,1.35208,103.82
-relay.moinsen.com,50.4754,12.3683
-relay03.lnfi.network,39.0997,-94.5786
+nostr.zenon.network,43.5009,-70.4428
+cyberspace.nostr1.com,40.7128,-74.006
+fenrir-s.notoshi.win,43.6532,-79.3832
+no.str.cr,9.92857,-84.0528
+relay.nostrdice.com,-33.8688,151.209
+nostr.data.haus,50.4754,12.3683
+relay.g1sms.fr,43.9432,2.07537
+relay.fundstr.me,42.3601,-71.0589
+nostr.thaliyal.com,40.8218,-74.45
+relay.mccormick.cx,52.3563,4.95714
+nostr.makibisskey.work,43.6532,-79.3832
+shu01.shugur.net,21.4902,39.2246
+relay.angor.io,48.1046,11.6002
+nostr.primz.org,43.6532,-79.3832
+nostr-02.yakihonne.com,1.32123,103.695
+relay.freeplace.nl,52.3676,4.90414
+relay.snort.social,43.6532,-79.3832
+nostr.rohoss.com,50.1109,8.68213
+relay.getsafebox.app,43.6532,-79.3832
+nostr.88mph.life,43.6532,-79.3832
+wot.sovbit.host,64.1466,-21.9426
+nostr-02.dorafactory.org,1.35208,103.82
+itanostr.space,52.2931,4.79099
+ribo.af.nostria.app,-26.2041,28.0473
+nostr.azzamo.net,52.2633,21.0283
+relay.fountain.fm,39.0997,-94.5786
relay.holzeis.me,43.6532,-79.3832
+relay.sincensura.org,43.6532,-79.3832
+nostr-dev.wellorder.net,45.5201,-122.99
+relay.hodl.ar,-32.94,-60.6745
+nostr-relay.psfoundation.info,39.0438,-77.4874
+relay.goodmorningbitcoin.com,43.6532,-79.3832
+nostr.rtvslawenia.com,49.4543,11.0746
+relay.13room.space,43.6532,-79.3832
+nostr-02.czas.top,53.471,9.88208
+relay.chorus.community,50.1109,8.68213
+shu04.shugur.net,25.2604,55.2989
+satsage.xyz,37.3986,-121.964
+relay.bullishbounty.com,43.6532,-79.3832
+nostr.sagaciousd.com,49.2827,-123.121
+relay.nostromo.social,49.4543,11.0746
+relay.magiccity.live,25.8128,-80.2377
relay-dev.satlantis.io,40.8302,-74.1299
+nostr.coincrowd.fund,39.0438,-77.4874
+shu05.shugur.net,48.8566,2.35222
+nostrelay.memory-art.xyz,43.6532,-79.3832
+nostr.now,36.55,139.733
+nostr-pub.wellorder.net,45.5201,-122.99
+nos.lol,50.4754,12.3683
+wot.dergigi.com,64.1476,-21.9392
+inbox.azzamo.net,52.2633,21.0283
+nostr.mikoshi.de,50.1109,8.68213
+librerelay.aaroniumii.com,43.6532,-79.3832
+relay.nosto.re,51.8933,4.42083
+relay.lumina.rocks,49.0291,8.35695
+kitchen.zap.cooking,43.6532,-79.3832
+zap.watch,45.5029,-73.5723
+nostr.spacecitynode.com,29.7057,-95.2706
+nostr.bilthon.dev,25.8128,-80.2377
+relay.aloftus.io,34.0881,-118.379
+shu02.shugur.net,21.4902,39.2246
+relay.mwaters.net,50.9871,2.12554
+relay.moinsen.com,50.4754,12.3683
+nostr.kungfu-g.rip,33.7946,-84.4488
+nostr.sathoarder.com,48.5734,7.75211
+nostream.breadslice.com,43.6532,-79.3832
+slick.mjex.me,39.048,-77.4817
+nostr.jerrynya.fun,31.2304,121.474
+articles.layer3.news,37.3387,-121.885
+nostr.satstralia.com,64.1476,-21.9392
+relay.degmods.com,50.4754,12.3683
+relay.0xchat.com,1.35208,103.82
+relay.nostx.io,43.6532,-79.3832
+relay.siamdev.cc,13.9178,100.424
+relay.mattybs.lol,43.6532,-79.3832
+relay.bitcoinveneto.org,64.1466,-21.9426
+nostr.tadryanom.me,43.6532,-79.3832
+nostr.spaceshell.xyz,43.6532,-79.3832
+relay.nostr.band,60.1699,24.9384
+relay03.lnfi.network,39.0997,-94.5786
+schnorr.me,43.6532,-79.3832
+khatru.nostrver.se,51.8933,4.42083
+dealtime-footwear-angry-records.trycloudflare.com,43.6532,-79.3832
+relay.endfiat.money,43.6532,-79.3832
+internationalright-wing.org,-22.5022,-48.7114
+nostr.mom,50.4754,12.3683
+nostrue.com,40.8054,-74.0241
+relay.chakany.systems,43.6532,-79.3832
+vidono.apps.slidestr.net,48.8566,2.35222
+relay.stream.labs.h3.se,59.4016,17.9455
+gnostr.com,40.9017,29.1616
+relay.origin.land,35.6673,139.751
+relay.sigit.io,50.4754,12.3683
+theoutpost.life,64.1476,-21.9392
+adre.su,59.9311,30.3609
+soloco.nl,43.6532,-79.3832
+wot.soundhsa.com,34.0479,-118.256
+nos.xmark.cc,50.6924,3.20113
+nostr-relay.zimage.com,34.282,-118.439
+nostr.pleb.one,38.6327,-90.1961
+purpura.cloud,43.6532,-79.3832
+ribo.us.nostria.app,41.5868,-93.625
+relay.digitalezukunft.cyou,45.5019,-73.5674
+relay.mess.ch,47.3591,8.55292
+relay.javi.space,43.4633,11.8796
+nostr.jfischer.org,49.0291,8.35696
+nostr.rblb.it,43.4633,11.8796
+relay.nostriot.com,41.5695,-83.9786
+nostr.21crypto.ch,47.4988,8.72369
+relay.nostr.vet,52.6467,4.7395
+fanfares.nostr1.com,40.7057,-74.0136
+relay.copylaradio.com,51.223,6.78245
+relay.mostro.network,40.8302,-74.1299
+nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832
+relay.lifpay.me,1.35208,103.82
+nostr.tac.lol,47.4748,-122.273
+relay.nostraddress.com,43.6532,-79.3832
+wot.sudocarlos.com,51.5072,-0.127586
+nostr.spicyz.io,43.6532,-79.3832
+relay02.lnfi.network,39.0997,-94.5786
+nostr.4rs.nl,49.0291,8.35696
+relay.21e6.cz,50.1682,14.0546
+nostr.casa21.space,43.6532,-79.3832
+wot.codingarena.top,50.4754,12.3683
+nostr-01.yakihonne.com,1.32123,103.695
+strfry.openhoofd.nl,51.9229,4.40833
+nostr.plantroon.com,50.1013,8.62643
+dev-nostr.bityacht.io,25.0797,121.234
+ribo.eu.nostria.app,52.3676,4.90414
+wot.nostr.place,30.2672,-97.7431
+relay.dwadziesciajeden.pl,52.2297,21.0122
+prl.plus,55.7623,37.6381
+relay.bitcoinartclock.com,50.4754,12.3683
+nostr.2b9t.xyz,34.0549,-118.243
+a.nos.lol,50.4754,12.3683
+relay.wavlake.com,41.2619,-95.8608
+relay.toastr.net,40.8054,-74.0241
+wheat.happytavern.co,43.6532,-79.3832
+relay.lnfi.network,39.0438,-77.4874
+nostr.liberty.fans,36.9104,-89.5875
+vitor.nostr1.com,40.7057,-74.0136
+nostr.thebiglake.org,32.71,-96.6745
+relay.exit.pub,50.4754,12.3683
+freelay.sovbit.host,64.1476,-21.9392
+purplerelay.com,50.1109,8.68213
+nostr.camalolo.com,24.1469,120.684
+black.nostrcity.club,41.8781,-87.6298
+relay.barine.co,43.6532,-79.3832
+nostr.0x7e.xyz,47.4988,8.72369
+relayone.soundhsa.com,34.0479,-118.256
+relay.vrtmrz.net,43.6532,-79.3832
+nostr-03.dorafactory.org,1.35208,103.82
+r.bitcoinhold.net,43.6532,-79.3832
+relay.agorist.space,52.3734,4.89406
+nostr-relay.amethyst.name,39.0067,-77.4291
+relay-rpi.edufeed.org,49.4543,11.0746
+orangesync.tech,50.1109,8.68213
+relay04.lnfi.network,39.0997,-94.5786
+nostr.hifish.org,47.4043,8.57398
+relay.nostr.wirednet.jp,34.706,135.493
+nostr.girino.org,43.6532,-79.3832
+strfry.felixzieger.de,50.1013,8.62643
+nostr.night7.space,50.4754,12.3683
+nostr.oxtr.dev,50.4754,12.3683
+relay.nostrhub.fr,48.1046,11.6002
+nostr.rikmeijer.nl,50.4754,12.3683
+wot.basspistol.org,49.4521,11.0767
+nostr.coincards.com,53.5501,-113.469
+solo.itsalldance.space,32.71,-96.6745
+relay.puresignal.news,43.6532,-79.3832
+nostrelay.circum.space,51.2217,6.77616
+relay.btcforplebs.com,43.6532,-79.3832
+nostr.diakod.com,43.6532,-79.3832
+tollbooth.stens.dev,51.223,6.78245
+relay.usefusion.ai,39.0438,-77.4874
+relay-admin.thaliyal.com,40.8218,-74.45
+nproxy.kristapsk.lv,60.1699,24.9384
+nostr-verified.wellorder.net,45.5201,-122.99
+wot.brightbolt.net,47.6735,-116.781
+relay.orangepill.ovh,49.1689,-0.358841
+relay.olas.app,50.4754,12.3683
+offchain.pub,36.1809,-115.241
+nostr.l484.com,30.2944,-97.6223
+relay.nostrhub.tech,49.4543,11.0746
+relay.tapestry.ninja,40.8054,-74.0241
+nostr.snowbla.de,60.1699,24.9384
+relay.letsfo.com,51.098,17.0321
+relay.tagayasu.xyz,43.6715,-79.38
+strfry.bonsai.com,37.8715,-122.273
+nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832
+nostr.smut.cloud,43.6532,-79.3832
+x.kojira.io,43.6532,-79.3832
+yabu.me,35.6092,139.73
+relay.zone667.com,60.1699,24.9384
+relay.bitcoindistrict.org,43.6532,-79.3832
+pyramid.fiatjaf.com,50.1109,8.68213
+nostr.hekster.org,37.3986,-121.964
+relay.laantungir.net,-19.4692,-42.5315
+relay.cosmicbolt.net,37.3986,-121.964
+strfry.shock.network,41.8959,-88.2169
+relay.coinos.io,43.6532,-79.3832
+wot.nostr.net,43.6532,-79.3832
+nostr.einundzwanzig.space,50.1109,8.68213
+premium.primal.net,43.6532,-79.3832
+dev-relay.lnfi.network,39.0997,-94.5786
+ynostr.yael.at,60.1699,24.9384
+relay.fr13nd5.com,52.5233,13.3426
+nostr.lojong.info,43.6532,-79.3832
+srtrelay.c-stellar.net,43.6532,-79.3832
+nostr.myshosholoza.co.za,52.3676,4.90414
+alien.macneilmediagroup.com,43.6532,-79.3832
+relay.utxo.farm,35.6916,139.768
+relay.ru.ac.th,13.7584,100.622
+relay2.angor.io,48.1046,11.6002
+relay.seq1.net,43.6532,-79.3832
+relay.illuminodes.com,47.6061,-122.333
+relay.satlantis.io,32.8769,-80.0114
+relay.primal.net,43.6532,-79.3832
+nostr.vulpem.com,49.4543,11.0746
+relay.evanverma.com,40.8302,-74.1299
+nostr.chaima.info,51.223,6.78245
+relay.notoshi.win,13.7829,100.546
+nostr.carroarmato0.be,50.9928,3.26317
+relay.ditto.pub,43.6532,-79.3832
+relay.credenso.cafe,43.1149,-80.7228
+relay.nostr.place,32.7767,-96.797
+relay.conduit.market,43.6532,-79.3832
+nostr.huszonegy.world,47.4979,19.0402
+nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874
+wot.nostr.party,36.1627,-86.7816
+wot.dtonon.com,43.6532,-79.3832
+nostr.middling.mydns.jp,35.8099,140.12
+nostr-2.21crypto.ch,47.4988,8.72369
+nostr.dlsouza.lol,50.1109,8.68213
From 6b54c70d26ad669064060b5d326d9cb1212400c8 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Sun, 7 Sep 2025 08:31:23 +0200
Subject: [PATCH 7/9] fix remove peer on disconnect (#388)
---
.../java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt
index f1c9009b..3494d1b2 100644
--- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt
+++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionTracker.kt
@@ -277,6 +277,7 @@ class BluetoothConnectionTracker(
fun cleanupDeviceConnection(deviceAddress: String) {
connectedDevices.remove(deviceAddress)?.let { deviceConn ->
subscribedDevices.removeAll { it.address == deviceAddress }
+ addressPeerMap.remove(deviceAddress)
}
pendingConnections.remove(deviceAddress)
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
From ba518269b47af5c705eed157982146e1ad3950c7 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Sun, 7 Sep 2025 09:04:21 +0200
Subject: [PATCH 8/9] fix geohash livedata wiring (#389)
---
.../android/nostr/NostrGeohashService.kt | 83 ++++---------------
.../java/com/bitchat/android/ui/ChatScreen.kt | 9 +-
.../com/bitchat/android/ui/ChatViewModel.kt | 7 --
.../com/bitchat/android/ui/MessageManager.kt | 14 +++-
4 files changed, 31 insertions(+), 82 deletions(-)
diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt b/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt
index cb98f732..f59e4434 100644
--- a/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt
+++ b/app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt
@@ -87,10 +87,8 @@ class NostrGeohashService(
private var geohashSamplingJob: Job? = null
private var geoParticipantsTimer: Job? = null
- // MARK: - Geohash Message History Properties
-
- private val geohashMessageHistory = mutableMapOf>() // geohash -> messages
- private val maxGeohashMessages = 1000 // Maximum messages per geohash
+ // MARK: - Geohash Message Routing
+ // Geohash messages are routed through MessageManager channel timelines using key "geo:"
// MARK: - Location Channel Management Properties
@@ -166,7 +164,6 @@ class NostrGeohashService(
try {
geohashParticipants.clear()
geoNicknames.clear()
- geohashMessageHistory.clear()
processedNostrEvents.clear()
processedNostrEventOrder.clear()
currentGeohashSubscriptionId = null
@@ -258,10 +255,8 @@ class NostrGeohashService(
powDifficulty = if (powSettingsLocal.enabled) powSettingsLocal.difficulty else null
)
- // Store immediately; UI will display from geohash history (not main mesh timeline)
- storeGeohashMessage(channel.geohash, localMessage)
- // IMPORTANT: Do not add to main mesh timeline to avoid duplication in mesh chat view
- // messageManager.addMessage(localMessage)
+ // Local echo into geohash channel timeline via unified MessageManager
+ messageManager.addChannelMessage("geo:${channel.geohash}", localMessage)
Log.d(TAG, "📝 Added geohash local echo with temp ID: $tempMessageId (not shown in mesh timeline)")
@@ -692,55 +687,7 @@ class NostrGeohashService(
}
}
- // MARK: - Geohash Message History
-
- /**
- * Store a message in geohash history
- */
- private fun storeGeohashMessage(geohash: String, message: BitchatMessage) {
- val messages = geohashMessageHistory.getOrPut(geohash) { mutableListOf() }
- messages.add(message)
-
- // Limit message history to prevent memory issues
- if (messages.size > maxGeohashMessages) {
- messages.removeAt(0) // Remove oldest message
- }
-
- Log.v(TAG, "📦 Stored message in geohash $geohash history (${messages.size} total) - sender: ${message.sender}, content: '${message.content.take(30)}...'")
- }
-
- /**
- * Load stored messages for a geohash channel
- */
- fun loadGeohashMessages(geohash: String) {
- val storedMessages = geohashMessageHistory[geohash]
- if (storedMessages == null) {
- Log.d(TAG, "📥 No stored messages found for geohash $geohash")
- return
- }
-
- Log.d(TAG, "📥 Loading ${storedMessages.size} stored messages for geohash $geohash")
-
- // Add all stored messages to the current message timeline
- storedMessages.forEach { message ->
- Log.v(TAG, "📥 Loading stored message: ${message.sender} - '${message.content.take(30)}...'")
- messageManager.addMessage(message)
- }
- }
-
- /**
- * Get stored messages for a geohash without mutating UI state
- */
- fun getGeohashMessages(geohash: String): List {
- return geohashMessageHistory[geohash]?.toList() ?: emptyList()
- }
-
- /**
- * Clear geohash message history
- */
- fun clearGeohashMessageHistory() {
- geohashMessageHistory.clear()
- }
+ // Geohash message history is maintained in ChatState.channelMessages; no local cache here
// MARK: - Geohash Participant Tracking
@@ -994,8 +941,7 @@ class NostrGeohashService(
private fun switchLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
// STEP 1: Immediate UI updates (synchronous, no blocking)
try {
- // NOTE: Don't clear messages here - let ChatScreen's displayMessages logic handle what to show
- // This preserves mesh message history when switching between views
+ // Do not clear messages here - preserve mesh history when switching between views
when (channel) {
is com.bitchat.android.geohash.ChannelID.Mesh -> {
@@ -1020,7 +966,10 @@ class NostrGeohashService(
// Clear notifications for this geohash since user is now viewing it
notificationManager.clearNotificationsForGeohash(channel.channel.geohash)
// Note: Don't clear geoNicknames - they contain cached nicknames for all geohashes
- // Note: Don't load messages here - ChatScreen will get them via getGeohashMessages()
+ // Clear unread for this geohash since user is now viewing it
+ try {
+ messageManager.clearChannelUnreadCount("geo:${channel.channel.geohash}")
+ } catch (_: Exception) { }
// Immediate self-registration for instant UI feedback
try {
@@ -1300,11 +1249,8 @@ class NostrGeohashService(
powDifficulty = actualPow.takeIf { it > 0 && eventHasNonseTag } ?: null
)
- // Store in geohash history for persistence across channel switches
- storeGeohashMessage(geohash, message)
-
- // NOTE: Don't add to main message timeline here - ChatScreen will display geohash messages
- // from the separate geohash history via getGeohashMessages()
+ // Add to geohash channel timeline (unified pipeline)
+ withContext(Dispatchers.Main) { messageManager.addChannelMessage("geo:$geohash", message) }
// NOTIFICATION LOGIC: Check for mentions and first messages
checkAndTriggerGeohashNotifications(geohash, senderName, content, message)
@@ -1403,8 +1349,9 @@ class NostrGeohashService(
* Check if this is the first message in a subscribed geohash chat
*/
private fun checkIfFirstMessage(geohash: String, message: BitchatMessage): Boolean {
- // Get the message history for this geohash
- val messageHistory = geohashMessageHistory[geohash] ?: return true
+ // Use ChatState.channelMessages for geohash history
+ val channelKey = "geo:$geohash"
+ val messageHistory = state.getChannelMessagesValue()[channelKey] ?: emptyList()
// Filter out our own messages (local echoes and messages from our identity)
val otherUserMessages = messageHistory.filter { msg ->
diff --git a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
index 6c0a5555..8e4e43b5 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt
@@ -74,18 +74,17 @@ fun ChatScreen(viewModel: ChatViewModel) {
// Get location channel info for timeline switching
val selectedLocationChannel by viewModel.selectedLocationChannel.observeAsState()
- // Determine what messages to show based on current context
+ // Determine what messages to show based on current context (unified timelines)
val displayMessages = when {
selectedPrivatePeer != null -> privateChats[selectedPrivatePeer] ?: emptyList()
currentChannel != null -> channelMessages[currentChannel] ?: emptyList()
else -> {
val locationChannel = selectedLocationChannel
if (locationChannel is com.bitchat.android.geohash.ChannelID.Location) {
- // For geohash channels, get messages from geohash history
- val geohash = locationChannel.channel.geohash
- viewModel.getGeohashMessages(geohash)
+ val geokey = "geo:${locationChannel.channel.geohash}"
+ channelMessages[geokey] ?: emptyList()
} else {
- messages // Mesh/public messages
+ messages // Mesh timeline
}
}
}
diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
index 74302cb9..89429b53 100644
--- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
+++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt
@@ -649,13 +649,6 @@ class ChatViewModel(
}
}
- /**
- * Get messages for a specific geohash timeline
- */
- fun getGeohashMessages(geohash: String): List {
- return nostrGeohashService.getGeohashMessages(geohash)
- }
-
/**
* Get participant count for a specific geohash (5-minute activity window)
*/
diff --git a/app/src/main/java/com/bitchat/android/ui/MessageManager.kt b/app/src/main/java/com/bitchat/android/ui/MessageManager.kt
index e554679d..276a6bbe 100644
--- a/app/src/main/java/com/bitchat/android/ui/MessageManager.kt
+++ b/app/src/main/java/com/bitchat/android/ui/MessageManager.kt
@@ -52,8 +52,18 @@ class MessageManager(private val state: ChatState) {
currentChannelMessages[channel] = channelMessageList
state.setChannelMessages(currentChannelMessages)
- // Update unread count if not currently in this channel
- if (state.getCurrentChannelValue() != channel) {
+ // Update unread count if not currently viewing this channel
+ // Consider both classic channels (state.currentChannel) and geohash location channel selection
+ val viewingClassicChannel = state.getCurrentChannelValue() == channel
+ val viewingGeohashChannel = try {
+ if (channel.startsWith("geo:")) {
+ val geo = channel.removePrefix("geo:")
+ val selected = state.selectedLocationChannel.value
+ selected is com.bitchat.android.geohash.ChannelID.Location && selected.channel.geohash.equals(geo, ignoreCase = true)
+ } else false
+ } catch (_: Exception) { false }
+
+ if (!viewingClassicChannel && !viewingGeohashChannel) {
val currentUnread = state.getUnreadChannelMessagesValue().toMutableMap()
currentUnread[channel] = (currentUnread[channel] ?: 0) + 1
state.setUnreadChannelMessages(currentUnread)
From 998ee606b14411fd7e1ebd3905f9d853bdef7673 Mon Sep 17 00:00:00 2001
From: callebtc <93376500+callebtc@users.noreply.github.com>
Date: Mon, 8 Sep 2025 13:46:15 +0200
Subject: [PATCH 9/9] Nostr refactor simplify (#390)
* fix bug
* geoDM receive works, send doesnt, and incoming message doesnt make sender appear in peer list
* fix nostr dm
* geohash dms work
* Geohash DM UI: stop mixing Nostr DM temp chats into mesh offline list; ensure geohash DM senders are added to geohash people list only. Removed nostr_* sidebar append in PeopleSection; kept 64-hex mesh offline favorites. Verified build.
* refactor
* nice
* works
* merging nostr -> mesh works
* tripple click to delete all
* fix sidebar icon
* remove hash
* dms have correct recipient
* works
* wip unread badge
* geohash dms wip
* dms work
---
.../favorites/FavoritesPersistenceService.kt | 241 +--
.../android/mesh/BluetoothMeshService.kt | 38 +-
.../bitchat/android/mesh/MessageHandler.kt | 2 +
.../android/nostr/GeohashAliasRegistry.kt | 24 +
.../android/nostr/GeohashMessageHandler.kt | 99 +
.../android/nostr/GeohashRepository.kt | 218 ++
.../nostr/NostrDirectMessageHandler.kt | 176 ++
.../android/nostr/NostrGeohashService.kt | 1788 -----------------
.../android/nostr/NostrSubscriptionManager.kt | 38 +
.../bitchat/android/nostr/NostrTransport.kt | 69 +-
.../bitchat/android/services/MessageRouter.kt | 30 +-
.../java/com/bitchat/android/ui/ChatHeader.kt | 75 +-
.../java/com/bitchat/android/ui/ChatState.kt | 17 +
.../com/bitchat/android/ui/ChatViewModel.kt | 191 +-
.../bitchat/android/ui/CommandProcessor.kt | 2 +-
.../bitchat/android/ui/GeohashViewModel.kt | 259 +++
.../bitchat/android/ui/MeshDelegateHandler.kt | 23 +-
.../bitchat/android/ui/PrivateChatManager.kt | 30 +-
.../bitchat/android/ui/SidebarComponents.kt | 71 +-
docs/ANNOUNCEMENT_GOSSIP.md | 83 +
docs/SOURCE_ROUTING.md | 78 +
21 files changed, 1509 insertions(+), 2043 deletions(-)
create mode 100644 app/src/main/java/com/bitchat/android/nostr/GeohashAliasRegistry.kt
create mode 100644 app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt
create mode 100644 app/src/main/java/com/bitchat/android/nostr/GeohashRepository.kt
create mode 100644 app/src/main/java/com/bitchat/android/nostr/NostrDirectMessageHandler.kt
delete mode 100644 app/src/main/java/com/bitchat/android/nostr/NostrGeohashService.kt
create mode 100644 app/src/main/java/com/bitchat/android/nostr/NostrSubscriptionManager.kt
create mode 100644 app/src/main/java/com/bitchat/android/ui/GeohashViewModel.kt
create mode 100644 docs/ANNOUNCEMENT_GOSSIP.md
create mode 100644 docs/SOURCE_ROUTING.md
diff --git a/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt b/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt
index f1366243..fe04c7b2 100644
--- a/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt
+++ b/app/src/main/java/com/bitchat/android/favorites/FavoritesPersistenceService.kt
@@ -9,11 +9,12 @@ import java.util.*
/**
* Bridging Noise and Nostr favorites
- * Direct port from iOS FavoritesPersistenceService.swift
+ * Direct port from iOS FavoritesPersistenceService.swift, with Android-specific
+ * peerID (16-hex) -> npub indexing for Nostr DM routing.
*/
data class FavoriteRelationship(
val peerNoisePublicKey: ByteArray, // Noise static public key (32 bytes)
- val peerNostrPublicKey: String?, // npub bech32 string
+ val peerNostrPublicKey: String?, // npub bech32 string
val peerNickname: String,
val isFavorite: Boolean, // We favorited them
val theyFavoritedUs: Boolean, // They favorited us
@@ -21,22 +22,22 @@ data class FavoriteRelationship(
val lastUpdated: Date
) {
val isMutual: Boolean get() = isFavorite && theyFavoritedUs
-
+
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
-
+
other as FavoriteRelationship
-
+
if (!peerNoisePublicKey.contentEquals(other.peerNoisePublicKey)) return false
if (peerNostrPublicKey != other.peerNostrPublicKey) return false
if (peerNickname != other.peerNickname) return false
if (isFavorite != other.isFavorite) return false
if (theyFavoritedUs != other.theyFavoritedUs) return false
-
+
return true
}
-
+
override fun hashCode(): Int {
var result = peerNoisePublicKey.contentHashCode()
result = 31 * result + (peerNostrPublicKey?.hashCode() ?: 0)
@@ -54,20 +55,21 @@ interface FavoritesChangeListener {
/**
* Manages favorites with Noise↔Nostr mapping
- * Singleton pattern matching iOS implementation
+ * Singleton pattern matching iOS implementation.
*/
class FavoritesPersistenceService private constructor(private val context: Context) {
-
+
companion object {
private const val TAG = "FavoritesPersistenceService"
- private const val FAVORITES_KEY = "favorite_relationships"
-
+ private const val FAVORITES_KEY = "favorite_relationships" // noiseHex -> relationship
+ private const val PEERID_INDEX_KEY = "favorite_peerid_index" // peerID(16-hex) -> npub
+
@Volatile
private var INSTANCE: FavoritesPersistenceService? = null
-
+
val shared: FavoritesPersistenceService
get() = INSTANCE ?: throw IllegalStateException("FavoritesPersistenceService not initialized")
-
+
fun initialize(context: Context) {
if (INSTANCE == null) {
synchronized(this) {
@@ -78,46 +80,40 @@ class FavoritesPersistenceService private constructor(private val context: Conte
}
}
}
-
+
private val stateManager = SecureIdentityStateManager(context)
private val gson = Gson()
- private val favorites = mutableMapOf() // noiseKeyHex -> relationship
+ private val favorites = mutableMapOf() // noiseHex -> relationship
+ // NEW: Index by current mesh peerID (16-hex) for direct lookup when sending Nostr DMs from mesh context
+ private val peerIdIndex = mutableMapOf() // peerID (lowercase 16-hex) -> npub
private val listeners = mutableListOf()
-
+
init {
loadFavorites()
+ loadPeerIdIndex()
}
-
- /**
- * Get favorite status for Noise public key
- */
+
+ /** Get favorite status for Noise public key */
fun getFavoriteStatus(noisePublicKey: ByteArray): FavoriteRelationship? {
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
return favorites[keyHex]
}
-
- /**
- * Get favorite status for 16-hex peerID
- */
+
+ /** Get favorite status for 16-hex peerID (by noiseHex prefix match) */
fun getFavoriteStatus(peerID: String): FavoriteRelationship? {
- // For 16-hex peerIDs, we need to find the corresponding full Noise key
- // This is a simplified lookup - in practice you'd use fingerprint matching
+ val pid = peerID.lowercase()
for ((_, relationship) in favorites) {
val noiseKeyHex = relationship.peerNoisePublicKey.joinToString("") { "%02x".format(it) }
- if (noiseKeyHex.startsWith(peerID)) {
- return relationship
- }
+ if (noiseKeyHex.startsWith(pid)) return relationship
}
return null
}
-
- /**
- * Update Nostr public key for a peer
- */
+
+ /** Update Nostr public key for a peer (indexed by Noise key) */
fun updateNostrPublicKey(noisePublicKey: ByteArray, nostrPubkey: String) {
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
val existing = favorites[keyHex]
-
+
if (existing != null) {
val updated = existing.copy(
peerNostrPublicKey = nostrPubkey,
@@ -125,7 +121,6 @@ class FavoritesPersistenceService private constructor(private val context: Conte
)
favorites[keyHex] = updated
} else {
- // Create new relationship
val relationship = FavoriteRelationship(
peerNoisePublicKey = noisePublicKey,
peerNostrPublicKey = nostrPubkey,
@@ -137,19 +132,54 @@ class FavoritesPersistenceService private constructor(private val context: Conte
)
favorites[keyHex] = relationship
}
-
+
saveFavorites()
notifyChanged(keyHex)
Log.d(TAG, "Updated Nostr pubkey association for ${keyHex.take(16)}...")
}
-
- /**
- * Update favorite status
- */
+
+ /** NEW: Update Nostr pubkey for specific mesh peerID (16-hex). */
+ fun updateNostrPublicKeyForPeerID(peerID: String, nostrPubkey: String) {
+ val pid = peerID.lowercase()
+ if (pid.length == 16 && pid.matches(Regex("^[0-9a-f]+$"))) {
+ peerIdIndex[pid] = nostrPubkey
+ savePeerIdIndex()
+ Log.d(TAG, "Indexed npub for peerID ${pid.take(8)}…")
+ } else {
+ Log.w(TAG, "updateNostrPublicKeyForPeerID called with non-16hex peerID: $peerID")
+ }
+ }
+
+ /** NEW: Resolve Nostr pubkey via current peerID mapping (fast path). */
+ fun findNostrPubkeyForPeerID(peerID: String): String? {
+ return peerIdIndex[peerID.lowercase()]
+ }
+
+ /** NEW: Resolve peerID (16-hex) for a given Nostr pubkey (npub or hex). */
+ fun findPeerIDForNostrPubkey(nostrPubkey: String): String? {
+ // First, try direct match in peerIdIndex (values are stored as npub strings)
+ peerIdIndex.entries.firstOrNull { it.value.equals(nostrPubkey, ignoreCase = true) }?.let { return it.key }
+
+ // Attempt legacy mapping via favorites Noise key association
+ val targetHex = normalizeNostrKeyToHex(nostrPubkey)
+ if (targetHex != null) {
+ // Find relationship with matching nostr pubkey (normalized to hex) and then try to map to current peerID via noise key prefix
+ val rel = favorites.values.firstOrNull { it.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex }
+ if (rel != null) {
+ val noiseHex = rel.peerNoisePublicKey.joinToString("") { "%02x".format(it) }
+ // Return 16-hex prefix as best-effort if no explicit mapping exists
+ return noiseHex.take(16)
+ }
+ }
+ return null
+ }
+
+ /** Update favorite status */
fun updateFavoriteStatus(noisePublicKey: ByteArray, nickname: String, isFavorite: Boolean) {
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
+
val existing = favorites[keyHex]
-
+
val updated = if (existing != null) {
existing.copy(
peerNickname = nickname,
@@ -168,21 +198,19 @@ class FavoritesPersistenceService private constructor(private val context: Conte
lastUpdated = Date()
)
}
-
+
favorites[keyHex] = updated
saveFavorites()
notifyChanged(keyHex)
Log.d(TAG, "Updated favorite status for $nickname: $isFavorite")
}
-
- /**
- * Update peer favorited us status
- */
+
+ /** Update peer favorited-us flag */
fun updatePeerFavoritedUs(noisePublicKey: ByteArray, theyFavoritedUs: Boolean) {
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
val existing = favorites[keyHex]
-
+
if (existing != null) {
val updated = existing.copy(
theyFavoritedUs = theyFavoritedUs,
@@ -191,101 +219,98 @@ class FavoritesPersistenceService private constructor(private val context: Conte
favorites[keyHex] = updated
saveFavorites()
notifyChanged(keyHex)
-
+
Log.d(TAG, "Updated peer favorited us for ${keyHex.take(16)}...: $theyFavoritedUs")
}
}
-
- /**
- * Get all mutual favorites
- */
- fun getMutualFavorites(): List {
- return favorites.values.filter { it.isMutual }
- }
-
- /**
- * Get all favorites we have
- */
- fun getOurFavorites(): List {
- return favorites.values.filter { it.isFavorite }
- }
-
- /**
- * Clear all favorites
- */
+
+ fun getMutualFavorites(): List = favorites.values.filter { it.isMutual }
+ fun getOurFavorites(): List = favorites.values.filter { it.isFavorite }
+
fun clearAllFavorites() {
favorites.clear()
saveFavorites()
+ peerIdIndex.clear()
+ savePeerIdIndex()
Log.i(TAG, "Cleared all favorites")
notifyAllCleared()
}
-
- /**
- * Find Noise key by Nostr pubkey
- */
+
+ /** Find Noise key by Nostr pubkey */
fun findNoiseKey(forNostrPubkey: String): ByteArray? {
val targetHex = normalizeNostrKeyToHex(forNostrPubkey) ?: return null
return favorites.values.firstOrNull { rel ->
- rel.peerNostrPublicKey?.let { stored ->
- normalizeNostrKeyToHex(stored)
- } == targetHex
+ rel.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex
}?.peerNoisePublicKey
}
-
- /**
- * Find Nostr pubkey by Noise key
- */
+
+ /** Find Nostr pubkey by Noise key */
fun findNostrPubkey(forNoiseKey: ByteArray): String? {
val keyHex = forNoiseKey.joinToString("") { "%02x".format(it) }
return favorites[keyHex]?.peerNostrPublicKey
}
-
- // MARK: - Private Methods
-
+
+ // MARK: - Persistence
+
private fun loadFavorites() {
try {
- // Use public methods instead of reflection to access encrypted preferences
val favoritesJson = stateManager.getSecureValue(FAVORITES_KEY)
if (favoritesJson != null) {
val type = object : TypeToken