From 1d80e64c694e3e670126230a92977eee83950636 Mon Sep 17 00:00:00 2001 From: aharshit123456 Date: Sun, 26 Jul 2026 14:08:37 +0530 Subject: [PATCH 01/45] fix: verify Schnorr signatures on incoming geohash Nostr events Geohash chat (kind 20000) and presence (kind 20001) events, and location notes (kind 1), were rendered/processed without ever calling NostrEvent.isValidSignature(). A relay-side attacker (or anyone able to publish to a connected relay) could forge an event claiming any victim's pubkey and have it accepted as genuine in GeohashMessageHandler, NostrClient.handleGeohashMessage, and LocationNotesManager.handleEvent. Add a signature check at all three ingestion points, dropping any event whose signature doesn't verify against its claimed pubkey before further processing (dedup, PoW check, participant tracking, or UI rendering). Adds GeohashMessageHandlerSignatureTest, which fails against the old code (forged event accepted and rendered) and passes against the fix, while confirming genuinely-signed events are unaffected. Fixes #733 Co-Authored-By: Claude Sonnet 5 --- .../android/nostr/GeohashMessageHandler.kt | 4 + .../android/nostr/LocationNotesManager.kt | 5 + .../com/bitchat/android/nostr/NostrClient.kt | 5 + .../GeohashMessageHandlerSignatureTest.kt | 95 +++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt index 1c4896f3..67599668 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt @@ -48,6 +48,10 @@ class GeohashMessageHandler( val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1) if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch if (dedupe(event.id)) return@launch + if (!event.isValidSignature()) { + Log.w(TAG, "Rejecting geohash event ${event.id.take(8)}... with invalid signature") + return@launch + } // PoW validation (if enabled) - apply to chat messages primarily if (event.kind == NostrKind.EPHEMERAL_EVENT) { diff --git a/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt b/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt index dc1a8e85..a8b907fd 100644 --- a/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/LocationNotesManager.kt @@ -361,6 +361,11 @@ class LocationNotesManager private constructor() { Log.v(TAG, "Ignoring non-text-note event: kind=${event.kind}") return } + + if (!event.isValidSignature()) { + Log.w(TAG, "Rejecting note ${event.id.take(8)}... with invalid signature") + return + } // Check for geohash tag val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" } diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrClient.kt b/app/src/main/java/com/bitchat/android/nostr/NostrClient.kt index a4803157..a3471662 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrClient.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrClient.kt @@ -282,6 +282,11 @@ class NostrClient private constructor(private val context: Context) { handler: (content: String, senderPubkey: String, nickname: String?, timestamp: Int) -> Unit ) { try { + if (!event.isValidSignature()) { + Log.w(TAG, "🚫 Rejecting geohash event ${event.id.take(8)}... with invalid signature") + return + } + // Check Proof of Work validation for incoming geohash events val powSettings = PoWPreferenceManager.getCurrentSettings() if (powSettings.enabled && powSettings.difficulty > 0) { diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt new file mode 100644 index 00000000..2f467dd9 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt @@ -0,0 +1,95 @@ +package com.bitchat.android.nostr + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import com.bitchat.android.ui.ChatState +import com.bitchat.android.ui.DataManager +import com.bitchat.android.ui.MessageManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +/** + * Regression coverage for GH-733: geohash events (kind 20000/20001) were accepted from the + * relay without Schnorr signature verification, allowing full pubkey impersonation. + */ +@RunWith(RobolectricTestRunner::class) +class GeohashMessageHandlerSignatureTest { + + private val application: Application = ApplicationProvider.getApplicationContext() + + @OptIn(ExperimentalCoroutinesApi::class) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var chatState: ChatState + private lateinit var dataManager: DataManager + private lateinit var messageManager: MessageManager + private lateinit var repo: GeohashRepository + private lateinit var handler: GeohashMessageHandler + + private val geohash = "u4pruy" + + @Before + fun setup() { + chatState = ChatState(scope = testScope) + dataManager = DataManager(context = application) + messageManager = MessageManager(state = chatState) + repo = GeohashRepository(application, chatState, dataManager) + handler = GeohashMessageHandler(application, chatState, messageManager, repo, testScope, dataManager) + } + + private fun buildSignedEvent(identity: NostrIdentity, content: String): NostrEvent { + val unsigned = NostrEvent( + pubkey = identity.publicKeyHex, + createdAt = (System.currentTimeMillis() / 1000).toInt(), + kind = NostrKind.EPHEMERAL_EVENT, + tags = listOf(listOf("g", geohash)), + content = content + ) + return unsigned.sign(identity.privateKeyHex) + } + + @Test + fun onEvent_acceptsGenuinelySignedEvent() { + val victim = NostrIdentity.generate() + val genuine = buildSignedEvent(victim, "hello from the real victim") + + handler.onEvent(genuine, geohash) + + val stored = chatState.getChannelMessagesValue()["geo:$geohash"] + assertEquals(1, stored?.size) + assertEquals("hello from the real victim", stored?.first()?.content) + } + + @Test + fun onEvent_rejectsEventWithForgedSignature() { + val victim = NostrIdentity.generate() + val attacker = NostrIdentity.generate() + + // Attacker claims the victim's pubkey but signs with their own key - a forged event. + val forged = buildSignedEvent(victim, "impersonated message").copy( + sig = NostrEvent( + pubkey = attacker.publicKeyHex, + createdAt = (System.currentTimeMillis() / 1000).toInt(), + kind = NostrKind.EPHEMERAL_EVENT, + tags = listOf(listOf("g", geohash)), + content = "impersonated message" + ).sign(attacker.privateKeyHex).sig + ) + + // Sanity check: this event is indeed invalid before we even touch the handler. + assertEquals(false, forged.isValidSignature()) + + handler.onEvent(forged, geohash) + + val stored = chatState.getChannelMessagesValue()["geo:$geohash"] + assertNull("Forged event must not be rendered as a legitimate message", stored) + } +} From 57a7d3d955bae6ae3bad97e6abf71e8625fc0447 Mon Sep 17 00:00:00 2001 From: aharshit123456 Date: Sun, 26 Jul 2026 14:48:27 +0530 Subject: [PATCH 02/45] fix: verify signature before dedup in GeohashMessageHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex review on PR #743: the previous ordering called dedupe(event.id) before isValidSignature(), so a forged event (bad signature, but a content-derived id matching a legitimate event) would get its id marked as "seen" and dropped — permanently poisoning the dedup cache. When the genuine, validly-signed copy of the same event later arrived (e.g. via a different relay), it was then silently discarded too, as an apparent duplicate. Move the signature check ahead of dedupe() so only genuinely-authenticated event ids are ever cached, matching the ordering already used correctly in NostrClient.handleGeohashMessage() and LocationNotesManager.handleEvent(). Adds a regression test that reproduces the exact scenario: a forged event is rejected first, then a genuine event with the same id must still be rendered (previously failed against the pre-fix ordering). Co-Authored-By: Claude Sonnet 5 --- .../android/nostr/GeohashMessageHandler.kt | 2 +- .../GeohashMessageHandlerSignatureTest.kt | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt index 67599668..19e5a369 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt @@ -47,11 +47,11 @@ class GeohashMessageHandler( if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1) if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch - if (dedupe(event.id)) return@launch if (!event.isValidSignature()) { Log.w(TAG, "Rejecting geohash event ${event.id.take(8)}... with invalid signature") return@launch } + if (dedupe(event.id)) return@launch // PoW validation (if enabled) - apply to chat messages primarily if (event.kind == NostrKind.EPHEMERAL_EVENT) { diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt index 2f467dd9..e0e2c8cd 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt @@ -92,4 +92,38 @@ class GeohashMessageHandlerSignatureTest { val stored = chatState.getChannelMessagesValue()["geo:$geohash"] assertNull("Forged event must not be rendered as a legitimate message", stored) } + + @Test + fun onEvent_stillAcceptsGenuineEventAfterForgedCopyWithSameIdWasRejected() { + val victim = NostrIdentity.generate() + val attacker = NostrIdentity.generate() + val genuine = buildSignedEvent(victim, "hello from the real victim") + + // A forged event carrying the SAME id as the genuine one (ids are a content hash, + // independent of the signature), but signed by an attacker - relays can deliver this + // before the genuine copy arrives from another relay. + val forgedWithSameId = genuine.copy( + sig = NostrEvent( + pubkey = attacker.publicKeyHex, + createdAt = genuine.createdAt, + kind = NostrKind.EPHEMERAL_EVENT, + tags = listOf(listOf("g", geohash)), + content = "hello from the real victim" + ).sign(attacker.privateKeyHex).sig + ) + assertEquals(genuine.id, forgedWithSameId.id) + assertEquals(false, forgedWithSameId.isValidSignature()) + + // Forged copy arrives first and must be rejected without poisoning the dedup cache. + handler.onEvent(forgedWithSameId, geohash) + assertNull(chatState.getChannelMessagesValue()["geo:$geohash"]) + + // The genuine copy (same id, valid signature) arrives afterwards from another relay - + // it must still be rendered, not silently dropped as a "duplicate". + handler.onEvent(genuine, geohash) + + val stored = chatState.getChannelMessagesValue()["geo:$geohash"] + assertEquals(1, stored?.size) + assertEquals("hello from the real victim", stored?.first()?.content) + } } From 7e2248073209d17d6b014e21091d2e2d3fe40f41 Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Thu, 30 Jul 2026 19:30:20 +0200 Subject: [PATCH 03/45] fix: verify geohash signatures off main thread --- .../bitchat/android/nostr/GeohashMessageHandler.kt | 11 +++++++++-- .../nostr/GeohashMessageHandlerSignatureTest.kt | 10 +++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt index 19e5a369..5d80e22d 100644 --- a/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/nostr/GeohashMessageHandler.kt @@ -5,8 +5,11 @@ import android.util.Log import com.bitchat.android.model.BitchatMessage import com.bitchat.android.ui.ChatState import com.bitchat.android.ui.MessageManager +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Date /** @@ -21,7 +24,8 @@ class GeohashMessageHandler( private val messageManager: MessageManager, private val repo: GeohashRepository, private val scope: CoroutineScope, - private val dataManager: com.bitchat.android.ui.DataManager + private val dataManager: com.bitchat.android.ui.DataManager, + private val signatureVerificationDispatcher: CoroutineDispatcher = Dispatchers.Default ) { companion object { private const val TAG = "GeohashMessageHandler" } @@ -47,7 +51,10 @@ class GeohashMessageHandler( if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1) if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch - if (!event.isValidSignature()) { + val hasValidSignature = withContext(signatureVerificationDispatcher) { + event.isValidSignature() + } + if (!hasValidSignature) { Log.w(TAG, "Rejecting geohash event ${event.id.take(8)}... with invalid signature") return@launch } diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt index e0e2c8cd..d9a557d8 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/GeohashMessageHandlerSignatureTest.kt @@ -42,7 +42,15 @@ class GeohashMessageHandlerSignatureTest { dataManager = DataManager(context = application) messageManager = MessageManager(state = chatState) repo = GeohashRepository(application, chatState, dataManager) - handler = GeohashMessageHandler(application, chatState, messageManager, repo, testScope, dataManager) + handler = GeohashMessageHandler( + application, + chatState, + messageManager, + repo, + testScope, + dataManager, + testDispatcher + ) } private fun buildSignedEvent(identity: NostrIdentity, content: String): NostrEvent { From e07e3c91186877f3d59414afd8cf1baac6413e4a Mon Sep 17 00:00:00 2001 From: a1denvalu3 Date: Thu, 30 Jul 2026 19:57:07 +0200 Subject: [PATCH 04/45] test: sign background geohash events --- .../NostrBackgroundEventProcessorTest.kt | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt index d7fffc2c..152d4ad0 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrBackgroundEventProcessorTest.kt @@ -36,17 +36,21 @@ class NostrBackgroundEventProcessorTest { fun `cold start processes more events than the removed handoff queue capacity`() = runBlocking { val application = ApplicationProvider.getApplicationContext() val processor = NostrBackgroundEventProcessor(application, scope) + val identity = NostrIdentity.generate() + val expectedIds = mutableSetOf() repeat(300) { index -> + val event = NostrEvent( + pubkey = identity.publicKeyHex, + createdAt = index + 1, + kind = NostrKind.EPHEMERAL_EVENT, + tags = listOf(listOf("g", "u4pruy")), + content = "message-$index" + ).sign(identity.privateKeyHex) + expectedIds += event.id + processor.onGeohashMessage( - event = NostrEvent( - id = "cold-start-$index", - pubkey = index.toString(16).padStart(64, '0'), - createdAt = 1, - kind = NostrKind.EPHEMERAL_EVENT, - tags = listOf(listOf("g", "u4pruy")), - content = "message-$index" - ), + event = event, geohash = "u4pruy" ) } @@ -58,7 +62,7 @@ class NostrBackgroundEventProcessorTest { } assertEquals( - (0 until 300).map { "cold-start-$it" }.toSet(), + expectedIds, AppStateStore.channelMessages.value["geo:u4pruy"].orEmpty().map { it.id }.toSet() ) } From 4b2a9c3b0d9fea512466e3a35c7384cb1c1638d0 Mon Sep 17 00:00:00 2001 From: areebahmeddd Date: Fri, 31 Jul 2026 04:59:22 +0530 Subject: [PATCH 05/45] preserve original bytes --- .../android/protocol/BinaryProtocol.kt | 53 ++++++++++++++++-- .../android/protocol/BinaryProtocolTest.kt | 55 +++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt index 47ee8d75..7a4725da 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -1,6 +1,7 @@ package com.bitchat.android.protocol import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.nio.ByteBuffer import java.nio.ByteOrder @@ -33,6 +34,27 @@ object SpecialRecipients { val BROADCAST = ByteArray(8) { 0xFF.toByte() } // All 0xFF = broadcast } +/** + * Payload as it arrived on the wire, set by [BinaryProtocol.decode]. + * + * Signatures cover a re-encoding of the packet, and verification re-encodes too. + * DEFLATE output is not canonical and clients use different encoders + * (java.util.zip.Deflater here, Apple's compression_encode_buffer on iOS), so + * re-compressing can change the preimage and reject a valid packet. Reusing these + * bytes also stops a relay, which re-encodes on TTL decrement, from substituting + * its own encoding. + * + * [forPayload] ties the bytes to the payload they decode to: replace the payload + * and the encoder compresses instead. + * + * Not a data class: generated equals/hashCode over ByteArray compares references. + */ +class WirePayload( + val bytes: ByteArray, + val compressed: Boolean, + val forPayload: ByteArray +) + /** * Binary packet format - 100% backward compatible with iOS version * @@ -60,7 +82,10 @@ data class BitchatPacket( val payload: ByteArray, var signature: ByteArray? = null, // Changed from val to var for packet signing var ttl: UByte, - var route: List? = null // Optional source route: ordered list of peerIDs (8 bytes each), not including sender and final recipient + var route: List? = null, // Optional source route: ordered list of peerIDs (8 bytes each), not including sender and final recipient + // Set by BinaryProtocol.decode. Not part of packet identity, so it stays out of + // the parcel, equals and hashCode. Losing it only costs a re-compression. + @IgnoredOnParcel val wirePayload: WirePayload? = null ) : Parcelable { constructor( @@ -99,7 +124,8 @@ data class BitchatPacket( payload = payload, signature = null, // Remove signature for signing route = route, - ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS // Use fixed TTL=0 for signing to ensure relay compatibility + ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS, // Use fixed TTL=0 for signing to ensure relay compatibility + wirePayload = wirePayload // preimage must use the originator's bytes ) return BinaryProtocol.encode(unsignedPacket) } @@ -213,7 +239,17 @@ object BinaryProtocol { var originalPayloadSize: Int? = null var isCompressed = false - if (CompressionUtil.shouldCompress(payload)) { + // Re-encode of a decoded packet: reuse the originator's bytes (see WirePayload). + val wire = packet.wirePayload + if (wire != null && wire.forPayload.contentEquals(packet.payload)) { + if (wire.compressed) { + payload = wire.bytes + originalPayloadSize = packet.payload.size + isCompressed = true + } + // Uncompressed on the wire: keep it that way. shouldCompress agrees + // across clients, but the "only if smaller" check need not. + } else if (CompressionUtil.shouldCompress(payload)) { CompressionUtil.compress(payload)?.let { compressedPayload -> originalPayloadSize = payload.size payload = compressedPayload @@ -449,6 +485,8 @@ object BinaryProtocol { } else null // Payload + // Kept so the packet can be re-encoded byte-identically (see WirePayload). + var receivedCompressed: ByteArray? = null val payload = if (isCompressed) { val lengthFieldBytes = if (version >= 2u.toUByte()) 4 else 2 if (payloadLength.toInt() < lengthFieldBytes) return null @@ -487,6 +525,8 @@ object BinaryProtocol { val expandedPayload = CompressionUtil.withDecompressionResources(resourceBytes) { val compressedPayload = ByteArray(compressedSize) buffer.get(compressedPayload) + // Captured here so the allocation stays inside the reservation. + receivedCompressed = compressedPayload decompress(compressedPayload, originalSize) } ?: return null if (expandedPayload.size != originalSize) { @@ -519,7 +559,12 @@ object BinaryProtocol { payload = payload, signature = signature, ttl = ttl, - route = route + route = route, + wirePayload = WirePayload( + bytes = receivedCompressed ?: payload, + compressed = receivedCompressed != null, + forPayload = payload + ) ) } catch (e: Exception) { diff --git a/app/src/test/java/com/bitchat/android/protocol/BinaryProtocolTest.kt b/app/src/test/java/com/bitchat/android/protocol/BinaryProtocolTest.kt index f6a1042d..c927257f 100644 --- a/app/src/test/java/com/bitchat/android/protocol/BinaryProtocolTest.kt +++ b/app/src/test/java/com/bitchat/android/protocol/BinaryProtocolTest.kt @@ -942,6 +942,61 @@ class BinaryProtocolTest { assertTrue("Payload must match", payload.contentEquals(decoded.payload)) } + /** + * Re-encoding preserves a payload compressed by a different DEFLATE encoder + * + * Verification re-encodes the packet, so a re-encode must reproduce the + * originator's bytes. iOS compresses with Apple's compression_encode_buffer, + * which need not match java.util.zip.Deflater byte for byte. + * + * The payload is wrapped in a raw-DEFLATE "stored" block (BTYPE=00): valid, it + * inflates correctly, and no compressor would emit it, so it stands in for a + * foreign encoder without a second compression library. Compared unpadded so + * the result does not depend on how padding is generated. + */ + @Test + fun `re-encoding preserves a foreign encoder's compressed payload`() { + val payload = "the mesh is up near the north gate. relay running all evening. " + .repeat(4) + .toByteArray() + + // Raw DEFLATE stored block: BFINAL=1 BTYPE=00, then LEN and NLEN little-endian. + val stored = ByteArray(5 + payload.size) + stored[0] = 0x01 + stored[1] = (payload.size and 0xFF).toByte() + stored[2] = ((payload.size shr 8) and 0xFF).toByte() + stored[3] = (payload.size.inv() and 0xFF).toByte() + stored[4] = ((payload.size.inv() shr 8) and 0xFF).toByte() + payload.copyInto(stored, 5) + + val buffer = ByteBuffer.allocate(1024).apply { order(ByteOrder.BIG_ENDIAN) } + buffer.put(2.toByte()) // version = 2 + buffer.put(MessageType.MESSAGE.value.toByte()) // type + buffer.put(5.toByte()) // ttl + buffer.putLong(fixedTimestamp.toLong()) // timestamp (8 bytes) + buffer.put(BinaryProtocol.Flags.IS_COMPRESSED.toByte()) // flags + buffer.putInt(stored.size + 4) // payloadLength incl. originalSize + buffer.put(hexToBytes(senderHex)) // senderID (8 bytes) + buffer.putInt(payload.size) // originalSize + buffer.put(stored) // compressed payload + + val raw = ByteArray(buffer.position()) + buffer.rewind() + buffer.get(raw) + + val decoded = BinaryProtocol.decode(raw) + assertNotNull("Foreign-encoded frame must decode", decoded) + assertArrayEquals("Payload must inflate to the original", payload, decoded!!.payload) + + val reEncoded = BinaryProtocol.encode(decoded, padding = false) + assertNotNull("Re-encode must succeed", reEncoded) + assertArrayEquals( + "Re-encode must reproduce the originator's bytes, or a valid signature stops verifying", + raw, + reEncoded + ) + } + /** * v2 compression bomb is rejected * From 83eaebc357b8d2148f42a075d57345fb31be2444 Mon Sep 17 00:00:00 2001 From: Kane Waldo Date: Fri, 31 Jul 2026 11:20:58 +0700 Subject: [PATCH 06/45] Fix geohash join channel routing --- .../com/bitchat/android/ui/ChannelManager.kt | 6 +++++- .../com/bitchat/android/ui/ChatViewModel.kt | 12 +++++++++++- .../bitchat/android/ui/CommandProcessorTest.kt | 18 ++++++++++++++++++ gradle/verification-metadata.xml | 6 ++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt b/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt index 5cc1375c..43bc2085 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import com.bitchat.android.geohash.ChannelID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.security.MessageDigest @@ -16,7 +17,8 @@ class ChannelManager( private val state: ChatState, private val messageManager: MessageManager, private val dataManager: DataManager, - private val coroutineScope: CoroutineScope + private val coroutineScope: CoroutineScope, + private val onSwitchToMeshLocation: () -> Unit = {} ) { // Channel encryption and security @@ -109,6 +111,8 @@ class ChannelManager( fun switchToChannel(channel: String?) { state.setCurrentChannel(channel) state.setSelectedPrivateChatPeer(null) + state.setSelectedLocationChannel(ChannelID.Mesh) + onSwitchToMeshLocation() // Clear unread count channel?.let { ch -> 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 16933537..24346e00 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -121,7 +121,17 @@ class ChatViewModel( private val conversationListPreferences = com.bitchat.android.services.ConversationListPreferences.getInstance(getApplication()) private val messageManager = MessageManager(state) - private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope) + private val channelManager = ChannelManager( + state, + messageManager, + dataManager, + viewModelScope, + onSwitchToMeshLocation = { + com.bitchat.android.geohash.LocationChannelManager + .getInstance(getApplication()) + .select(com.bitchat.android.geohash.ChannelID.Mesh) + } + ) // Create Noise session delegate for clean dependency injection private val noiseSessionDelegate = object : NoiseSessionDelegate { diff --git a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt index 011f80a2..fee53c0d 100644 --- a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt @@ -161,4 +161,22 @@ class CommandProcessorTest() { ) assertEquals(0, chatState.getMessagesValue().size) } + + @Test + fun `join command leaves active geohash selection for mesh channel routing`() { + chatState.setSelectedLocationChannel( + ChannelID.Location(GeohashChannel(GeohashChannelLevel.REGION, "9q")) + ) + + commandProcessor.processCommand( + command = "/join backchannel", + meshService = meshService, + myPeerID = "peer-id", + onSendMessage = { _, _, _ -> }, + viewModel = null + ) + + assertEquals("#backchannel", chatState.getCurrentChannelValue()) + assertEquals(ChannelID.Mesh, chatState.selectedLocationChannel.value) + } } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a62cbff6..08eb23c4 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2147,6 +2147,9 @@ + + + @@ -4903,6 +4906,9 @@ + + + From ac47af9101d25d017de37bda92683754da7e6d55 Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 10 Aug 2026 20:09:00 +0530 Subject: [PATCH 07/45] fix: synchronize totalChecks increment in NostrEventDeduplicator isDuplicate() incremented totalChecks outside the lruLock that guards every other mutation on this class, so concurrent callers raced on the read-modify-write and lost increments. duplicateCount and evictionCount were already incremented under the lock; totalChecks was the one counter left outside it. getStats().totalChecks feeds hitRate and is the only denominator for duplicateCount, so a systematic undercount skews both. Moved the increment inside the existing synchronized block. Added a concurrency test that reproduces the race: run against the prior code it failed reliably (3/3 runs); against the fix it passes. --- .../android/nostr/NostrEventDeduplicator.kt | 4 +- .../nostr/NostrEventDeduplicatorTest.kt | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/src/test/kotlin/com/bitchat/android/nostr/NostrEventDeduplicatorTest.kt diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrEventDeduplicator.kt b/app/src/main/java/com/bitchat/android/nostr/NostrEventDeduplicator.kt index 0638eafd..138ffb0a 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrEventDeduplicator.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrEventDeduplicator.kt @@ -79,9 +79,9 @@ class NostrEventDeduplicator( * @return true if the event is a duplicate (already seen), false if it's new */ fun isDuplicate(eventId: String): Boolean { - totalChecks++ - synchronized(lruLock) { + totalChecks++ + val existingNode = nodeMap[eventId] if (existingNode != null) { diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrEventDeduplicatorTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrEventDeduplicatorTest.kt new file mode 100644 index 00000000..557ead2e --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrEventDeduplicatorTest.kt @@ -0,0 +1,66 @@ +package com.bitchat.android.nostr + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class NostrEventDeduplicatorTest { + @Test + fun `isDuplicate flags a repeated event id`() { + val deduplicator = NostrEventDeduplicator(maxCapacity = 10) + + assertEquals(false, deduplicator.isDuplicate("event-1")) + assertEquals(true, deduplicator.isDuplicate("event-1")) + } + + @Test + fun `capacity evicts the least recently used event id`() { + val deduplicator = NostrEventDeduplicator(maxCapacity = 2) + + deduplicator.isDuplicate("a") + deduplicator.isDuplicate("b") + deduplicator.isDuplicate("c") // evicts "a" + + assertTrue(deduplicator.contains("b")) + assertTrue(deduplicator.contains("c")) + assertEquals(false, deduplicator.contains("a")) + } + + /** + * totalChecks used to be incremented outside the lruLock that guards every other + * mutation in this class, so concurrent callers could race on the read-modify-write + * and lose increments. Every other counter (duplicateCount, evictionCount) was + * already incremented under the lock, which is why only this one drifted. + */ + @Test + fun `totalChecks counts every call exactly once under concurrent access`() { + val deduplicator = NostrEventDeduplicator(maxCapacity = 10_000) + val threadCount = 8 + val checksPerThread = 2_000 + val executor = Executors.newFixedThreadPool(threadCount) + val start = CountDownLatch(1) + val done = CountDownLatch(threadCount) + + repeat(threadCount) { threadIndex -> + executor.submit { + start.await() + repeat(checksPerThread) { callIndex -> + deduplicator.isDuplicate("thread-$threadIndex-event-$callIndex") + } + done.countDown() + } + } + + start.countDown() + assertTrue(done.await(10, TimeUnit.SECONDS)) + executor.shutdown() + + assertEquals( + (threadCount * checksPerThread).toLong(), + deduplicator.getStats().totalChecks + ) + } +} From 4007bae1e23f1d9945d4bd04f77b6e52d4396353 Mon Sep 17 00:00:00 2001 From: Kane Waldo Date: Tue, 11 Aug 2026 09:19:43 +0700 Subject: [PATCH 08/45] Fix syntax error in CommandProcessorTest --- .../android/ui/CommandProcessorTest.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt index 2ed944fe..985e9a9f 100644 --- a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt @@ -24,11 +24,12 @@ import org.robolectric.RobolectricTestRunner import java.util.Date @RunWith(RobolectricTestRunner::class) -class CommandProcessorTest() { +class CommandProcessorTest { private val context: Context = ApplicationProvider.getApplicationContext() - @OptIn(ExperimentalCoroutinesApi::class) - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + + @OptIn(ExperimentalCoroutinesApi::class) + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) private val chatState = ChatState(scope = testScope) private lateinit var commandProcessor: CommandProcessor @@ -63,11 +64,11 @@ class CommandProcessorTest() { val channel = "channel-1" val result = commandProcessor.processCommand( - command = "/j $channel", - meshService = meshService, - myPeerID = "peer-id", - onSendMessage = { a, b, c -> { } }, - viewModel = null + command = "/j $channel", + meshService = meshService, + myPeerID = "peer-id", + onSendMessage = { a, b, c -> { } }, + viewModel = null ) assertEquals(result, true) @@ -206,4 +207,4 @@ class CommandProcessorTest() { assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) } -} +} \ No newline at end of file From 76bf226f9886f9480fde32a055d802e27240769c Mon Sep 17 00:00:00 2001 From: wollow Date: Tue, 11 Aug 2026 14:52:16 +0300 Subject: [PATCH 09/45] fix(nostr): cap DM envelope backdating at 24 hours --- .../com/bitchat/android/nostr/NostrCrypto.kt | 4 +- .../bitchat/android/nostr/NostrProtocol.kt | 2 +- .../android/nostr/NostrProtocolTest.kt | 46 +++++++++++++++++++ docs/client-rewrite-contracts.md | 7 ++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt index b25499dd..972b90fa 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt @@ -317,9 +317,9 @@ object NostrCrypto { } /** - * Random timestamp up to maxPastSeconds in the past (default 2 days) + * Random timestamp up to maxPastSeconds in the past (default 24 hours for iOS compatibility) */ - fun randomizeTimestampUpToPast(maxPastSeconds: Int = 172800): Int { + fun randomizeTimestampUpToPast(maxPastSeconds: Int = 86400): Int { val now = (System.currentTimeMillis() / 1000).toInt() val offset = if (maxPastSeconds > 0) secureRandom.nextInt(maxPastSeconds + 1) else 0 return now - offset diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt index 4376d1e9..926ea493 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt @@ -37,7 +37,7 @@ object NostrProtocol { val rumorId = rumorBase.computeEventIdHex() val rumor = rumorBase.copy(id = rumorId) - // 2. Seal the rumor (kind 13) signed by sender, timestamp randomized up to 2 days + // 2. Seal the rumor (kind 13) signed by sender, timestamp randomized within iOS's 24h lookback val sealedEvent = createSeal( rumor = rumor, recipientPubkey = recipientPubkey, diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt index a5bd9561..3ef73733 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt @@ -3,6 +3,7 @@ package com.bitchat.android.nostr import com.google.gson.Gson import org.junit.Assert.assertEquals import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test class NostrProtocolTest { @@ -41,6 +42,47 @@ class NostrProtocolTest { assertNull(decrypted) } + @Test + fun createPrivateMessage_limitsEnvelopeTimestampsToIosLookback() { + val sender = NostrIdentity.generate() + val recipient = NostrIdentity.generate() + + repeat(20) { + val beforeCreation = (System.currentTimeMillis() / 1000).toInt() + val giftWrap = NostrProtocol.createPrivateMessage( + content = "bitchat1:test", + recipientPubkey = recipient.publicKeyHex, + senderIdentity = sender + ).single() + val afterCreation = (System.currentTimeMillis() / 1000).toInt() + val sealJson = NostrCrypto.decryptNIP44( + ciphertext = giftWrap.content, + senderPublicKeyHex = giftWrap.pubkey, + recipientPrivateKeyHex = recipient.privateKeyHex + ) + val seal = gson.fromJson(sealJson, NostrEvent::class.java) + + assertTimestampWithinIosLookback("gift wrap", giftWrap.createdAt, beforeCreation, afterCreation) + assertTimestampWithinIosLookback("seal", seal.createdAt, beforeCreation, afterCreation) + } + } + + private fun assertTimestampWithinIosLookback( + envelope: String, + createdAt: Int, + beforeCreation: Int, + afterCreation: Int + ) { + assertTrue( + "$envelope timestamp must be no more than 24 hours old", + createdAt >= beforeCreation - IOS_DM_LOOKBACK_SECONDS + ) + assertTrue( + "$envelope timestamp must not be in the future", + createdAt <= afterCreation + ) + } + private fun forgedGiftWrap( content: String, claimedSender: NostrIdentity, @@ -82,4 +124,8 @@ class NostrProtocolTest { content = giftWrapContent ).sign(wrapPrivateKey) } + + private companion object { + const val IOS_DM_LOOKBACK_SECONDS = 86400 + } } diff --git a/docs/client-rewrite-contracts.md b/docs/client-rewrite-contracts.md index 1abed243..1da0e33a 100644 --- a/docs/client-rewrite-contracts.md +++ b/docs/client-rewrite-contracts.md @@ -17,7 +17,7 @@ The remaining implementation work and milestone progress are tracked in | Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` | | Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` | | Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` | -| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | +| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 24-hour outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | | Application state | Peer unions, canonical private conversations, chronological history, delivery/read behavior, media migration policy | `AppStateStoreTest`, `PrivateChatManagerTest`, `MediaSendingManagerMigrationTest` | ## Golden-vector policy @@ -31,6 +31,11 @@ Round-trip tests remain useful but are not sufficient on their own: an encoder and decoder can share the same defect. Each critical wire format therefore has at least one literal vector. +NIP-17 receivers must use a lookback at least as long as the maximum timestamp +randomization used by senders. Android caps outbound seal and gift-wrap +randomization at 24 hours for iOS interoperability while retaining its 48-hour +receive lookback. + ## Rewrite acceptance gate From a configured Android development environment, run: From bc03d789720707fd106cb0b4127dd8c2229e67d9 Mon Sep 17 00:00:00 2001 From: wollow Date: Tue, 11 Aug 2026 17:57:03 +0300 Subject: [PATCH 10/45] changed to 23-hour-45-minute maximum backdating window --- .../com/bitchat/android/nostr/NostrCrypto.kt | 8 +++++--- .../com/bitchat/android/nostr/NostrProtocol.kt | 2 +- .../bitchat/android/nostr/NostrProtocolTest.kt | 16 ++++++++++++---- docs/client-rewrite-contracts.md | 6 +++--- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt index 972b90fa..cab41ad0 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt @@ -21,7 +21,9 @@ import java.math.BigInteger * Includes secp256k1 operations, ECDH, and NIP-44 encryption */ object NostrCrypto { - + + internal const val NIP17_DEFAULT_MAX_PAST_SECONDS = 85_500 + private val secureRandom = SecureRandom() // NIP-44 v2 only @@ -317,9 +319,9 @@ object NostrCrypto { } /** - * Random timestamp up to maxPastSeconds in the past (default 24 hours for iOS compatibility) + * Random timestamp in the past, defaulting to 23h45m to leave slack inside iOS's 24h lookback. */ - fun randomizeTimestampUpToPast(maxPastSeconds: Int = 86400): Int { + fun randomizeTimestampUpToPast(maxPastSeconds: Int = NIP17_DEFAULT_MAX_PAST_SECONDS): Int { val now = (System.currentTimeMillis() / 1000).toInt() val offset = if (maxPastSeconds > 0) secureRandom.nextInt(maxPastSeconds + 1) else 0 return now - offset diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt index 926ea493..42caefec 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt @@ -37,7 +37,7 @@ object NostrProtocol { val rumorId = rumorBase.computeEventIdHex() val rumor = rumorBase.copy(id = rumorId) - // 2. Seal the rumor (kind 13) signed by sender, timestamp randomized within iOS's 24h lookback + // 2. Seal the rumor with 15 minutes of slack inside iOS's 24-hour lookback. val sealedEvent = createSeal( rumor = rumor, recipientPubkey = recipientPubkey, diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt index 3ef73733..29a41f6e 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt @@ -43,10 +43,15 @@ class NostrProtocolTest { } @Test - fun createPrivateMessage_limitsEnvelopeTimestampsToIosLookback() { + fun createPrivateMessage_reservesSlackInsideIosLookback() { val sender = NostrIdentity.generate() val recipient = NostrIdentity.generate() + assertEquals( + IOS_DM_LOOKBACK_SECONDS - TIMESTAMP_SAFETY_SLACK_SECONDS, + NostrCrypto.NIP17_DEFAULT_MAX_PAST_SECONDS + ) + repeat(20) { val beforeCreation = (System.currentTimeMillis() / 1000).toInt() val giftWrap = NostrProtocol.createPrivateMessage( @@ -74,8 +79,8 @@ class NostrProtocolTest { afterCreation: Int ) { assertTrue( - "$envelope timestamp must be no more than 24 hours old", - createdAt >= beforeCreation - IOS_DM_LOOKBACK_SECONDS + "$envelope timestamp must leave 15 minutes inside the iOS lookback", + createdAt >= beforeCreation - MAX_OUTBOUND_BACKDATE_SECONDS ) assertTrue( "$envelope timestamp must not be in the future", @@ -126,6 +131,9 @@ class NostrProtocolTest { } private companion object { - const val IOS_DM_LOOKBACK_SECONDS = 86400 + const val IOS_DM_LOOKBACK_SECONDS = 86_400 + const val TIMESTAMP_SAFETY_SLACK_SECONDS = 900 + const val MAX_OUTBOUND_BACKDATE_SECONDS = + IOS_DM_LOOKBACK_SECONDS - TIMESTAMP_SAFETY_SLACK_SECONDS } } diff --git a/docs/client-rewrite-contracts.md b/docs/client-rewrite-contracts.md index 1da0e33a..ed92ab79 100644 --- a/docs/client-rewrite-contracts.md +++ b/docs/client-rewrite-contracts.md @@ -17,7 +17,7 @@ The remaining implementation work and milestone progress are tracked in | Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` | | Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` | | Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` | -| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 24-hour outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | +| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 23h45m outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | | Application state | Peer unions, canonical private conversations, chronological history, delivery/read behavior, media migration policy | `AppStateStoreTest`, `PrivateChatManagerTest`, `MediaSendingManagerMigrationTest` | ## Golden-vector policy @@ -33,8 +33,8 @@ at least one literal vector. NIP-17 receivers must use a lookback at least as long as the maximum timestamp randomization used by senders. Android caps outbound seal and gift-wrap -randomization at 24 hours for iOS interoperability while retaining its 48-hour -receive lookback. +randomization at 23h45m, leaving 15 minutes of slack inside iOS's 24-hour +subscription window, while retaining its 48-hour receive lookback. ## Rewrite acceptance gate From acfbacf78fefb6664a523111f2c8c73a2e85b67f Mon Sep 17 00:00:00 2001 From: wollow Date: Tue, 11 Aug 2026 22:37:44 +0300 Subject: [PATCH 11/45] fix(nostr): reserve two-hour DM timestamp margin --- app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt | 4 ++-- .../main/java/com/bitchat/android/nostr/NostrProtocol.kt | 2 +- .../kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt | 4 ++-- docs/client-rewrite-contracts.md | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt index cab41ad0..1bbdc972 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrCrypto.kt @@ -22,7 +22,7 @@ import java.math.BigInteger */ object NostrCrypto { - internal const val NIP17_DEFAULT_MAX_PAST_SECONDS = 85_500 + internal const val NIP17_DEFAULT_MAX_PAST_SECONDS = 79_200 private val secureRandom = SecureRandom() // NIP-44 v2 only @@ -319,7 +319,7 @@ object NostrCrypto { } /** - * Random timestamp in the past, defaulting to 23h45m to leave slack inside iOS's 24h lookback. + * Random timestamp in the past, defaulting to 22 hours to leave 2 hours of slack inside iOS's 24-hour lookback. */ fun randomizeTimestampUpToPast(maxPastSeconds: Int = NIP17_DEFAULT_MAX_PAST_SECONDS): Int { val now = (System.currentTimeMillis() / 1000).toInt() diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt index 42caefec..7463a378 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrProtocol.kt @@ -37,7 +37,7 @@ object NostrProtocol { val rumorId = rumorBase.computeEventIdHex() val rumor = rumorBase.copy(id = rumorId) - // 2. Seal the rumor with 15 minutes of slack inside iOS's 24-hour lookback. + // 2. Seal the rumor with 2 hours of slack inside iOS's 24-hour lookback. val sealedEvent = createSeal( rumor = rumor, recipientPubkey = recipientPubkey, diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt index 29a41f6e..5faae233 100644 --- a/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/nostr/NostrProtocolTest.kt @@ -79,7 +79,7 @@ class NostrProtocolTest { afterCreation: Int ) { assertTrue( - "$envelope timestamp must leave 15 minutes inside the iOS lookback", + "$envelope timestamp must leave 2 hours inside the iOS lookback", createdAt >= beforeCreation - MAX_OUTBOUND_BACKDATE_SECONDS ) assertTrue( @@ -132,7 +132,7 @@ class NostrProtocolTest { private companion object { const val IOS_DM_LOOKBACK_SECONDS = 86_400 - const val TIMESTAMP_SAFETY_SLACK_SECONDS = 900 + const val TIMESTAMP_SAFETY_SLACK_SECONDS = 7_200 const val MAX_OUTBOUND_BACKDATE_SECONDS = IOS_DM_LOOKBACK_SECONDS - TIMESTAMP_SAFETY_SLACK_SECONDS } diff --git a/docs/client-rewrite-contracts.md b/docs/client-rewrite-contracts.md index ed92ab79..05d5e1ad 100644 --- a/docs/client-rewrite-contracts.md +++ b/docs/client-rewrite-contracts.md @@ -17,7 +17,7 @@ The remaining implementation work and milestone progress are tracked in | Inner payloads | Noise type bytes, private-message TLVs, peer-state TLVs, file-transfer TLVs, live-voice bursts, fragment header, sync request TLVs | `ClientRewriteWireContractTest`, `AuthenticatedPeerStateTest`, `PrivateMediaTransferPreparerTest`, `VoiceBurstPacketTest`, `FragmentManagerTest` | | Identity/security | Announcement extensions, capability bitfield endianness, Noise static-key binding, handshake identity binding, signatures | `IdentityAnnouncementTest`, `NoiseSessionManagerIdentityBindingTest`, `ClientRewritePrimitiveContractTest` | | Sync/routing | Stable packet IDs, GCS bitstream, replay collapse, TTL handling, relay choice, confirmed graph edges | `ClientRewritePrimitiveContractTest`, `GCSFilterTest`, `PacketRelayManagerTest`, `MeshGraphServiceTest`, `TransportBridgeServiceTest` | -| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 23h45m outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | +| Nostr | Bech32, secp256k1 key derivation, NIP-01 event IDs/signatures, NIP-44 authenticated encryption, NIP-13 PoW, authenticated NIP-17 seals, 22h outbound envelope randomization | `ClientRewriteNostrContractTest`, `NostrProtocolTest` | | Application state | Peer unions, canonical private conversations, chronological history, delivery/read behavior, media migration policy | `AppStateStoreTest`, `PrivateChatManagerTest`, `MediaSendingManagerMigrationTest` | ## Golden-vector policy @@ -31,9 +31,9 @@ Round-trip tests remain useful but are not sufficient on their own: an encoder and decoder can share the same defect. Each critical wire format therefore has at least one literal vector. -NIP-17 receivers must use a lookback at least as long as the maximum timestamp +NIP-17 receivers should reserve safety slack beyond the maximum timestamp randomization used by senders. Android caps outbound seal and gift-wrap -randomization at 23h45m, leaving 15 minutes of slack inside iOS's 24-hour +randomization at 22h, leaving 2 hours of slack inside iOS's 24-hour subscription window, while retaining its 48-hour receive lookback. ## Rewrite acceptance gate From 22f87a493a982d5b4fd0909f1360bbb3a74a0be5 Mon Sep 17 00:00:00 2001 From: wollow Date: Thu, 13 Aug 2026 15:11:54 +0300 Subject: [PATCH 12/45] Add announce capability bit assignments --- .../bitchat/android/model/PeerCapabilities.kt | 14 ++++++++ .../android/model/IdentityAnnouncementTest.kt | 34 +++++++++++++++++-- docs/ANNOUNCEMENT_GOSSIP.md | 13 +++++-- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt b/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt index d53f3d20..e64fc390 100644 --- a/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt +++ b/app/src/main/java/com/bitchat/android/model/PeerCapabilities.kt @@ -29,9 +29,23 @@ data class PeerCapabilities(val rawValue: Long) : Parcelable { companion object { val NONE = PeerCapabilities(0) + val PREKEYS = PeerCapabilities(1L shl 0) + val WIFI_BULK = PeerCapabilities(1L shl 1) + val GATEWAY = PeerCapabilities(1L shl 2) + val GROUPS = PeerCapabilities(1L shl 3) + val BOARD = PeerCapabilities(1L shl 4) + val VOUCH = PeerCapabilities(1L shl 5) + val MESH_DIAGNOSTICS = PeerCapabilities(1L shl 6) + val BRIDGE = PeerCapabilities(1L shl 7) + /** Noise-encrypted private BitchatFilePacket using payload type 0x20. */ val PRIVATE_MEDIA = PeerCapabilities(1L shl 8) + val PRIVATE_MEDIA_RECEIPTS = PeerCapabilities(1L shl 9) + + /** Reserved by iOS; decode it but do not advertise or act on it. */ + val NON_DESTRUCTIVE_NOISE_REPLACEMENT = PeerCapabilities(1L shl 10) + /** Capabilities implemented by this Android build. */ val LOCAL_SUPPORTED = PRIVATE_MEDIA diff --git a/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt b/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt index 78d72bee..892f6565 100644 --- a/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/model/IdentityAnnouncementTest.kt @@ -12,9 +12,38 @@ class IdentityAnnouncementTest { private val signingKey = ByteArray(32) { 0x22 } @Test - fun `private media capability uses iOS little-endian bytes`() { + fun `capability assignments and encoding match iOS`() { + assertEquals(1L shl 0, PeerCapabilities.PREKEYS.rawValue) + assertEquals(1L shl 1, PeerCapabilities.WIFI_BULK.rawValue) + assertEquals(1L shl 2, PeerCapabilities.GATEWAY.rawValue) + assertEquals(1L shl 3, PeerCapabilities.GROUPS.rawValue) + assertEquals(1L shl 4, PeerCapabilities.BOARD.rawValue) + assertEquals(1L shl 5, PeerCapabilities.VOUCH.rawValue) + assertEquals(1L shl 6, PeerCapabilities.MESH_DIAGNOSTICS.rawValue) + assertEquals(1L shl 7, PeerCapabilities.BRIDGE.rawValue) + assertEquals(1L shl 8, PeerCapabilities.PRIVATE_MEDIA.rawValue) + assertEquals(1L shl 9, PeerCapabilities.PRIVATE_MEDIA_RECEIPTS.rawValue) + assertEquals(1L shl 10, PeerCapabilities.NON_DESTRUCTIVE_NOISE_REPLACEMENT.rawValue) + + val assigned = PeerCapabilities((1L shl 11) - 1) + assertArrayEquals(byteArrayOf(0xFF.toByte(), 0x07), assigned.encoded()) + assertEquals(assigned, PeerCapabilities.decode(assigned.encoded())) + } + + @Test + fun `capability encoding is minimal and preserves low 64 unknown bits`() { + assertArrayEquals(byteArrayOf(0x00), PeerCapabilities.NONE.encoded()) + assertArrayEquals(byteArrayOf(0x01), PeerCapabilities.PREKEYS.encoded()) assertArrayEquals(byteArrayOf(0x00, 0x01), PeerCapabilities.PRIVATE_MEDIA.encoded()) - assertTrue(PeerCapabilities.decode(byteArrayOf(0x00, 0x01)).contains(PeerCapabilities.PRIVATE_MEDIA)) + + val future = PeerCapabilities.decode( + byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80.toByte(), 0x55) + ) + assertEquals(Long.MIN_VALUE or 1L, future.rawValue) + assertArrayEquals( + byteArrayOf(0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80.toByte()), + future.encoded() + ) } @Test @@ -71,5 +100,6 @@ class IdentityAnnouncementTest { .capabilities!! .contains(PeerCapabilities.PRIVATE_MEDIA) ) + assertEquals(PeerCapabilities.PRIVATE_MEDIA, PeerCapabilities.LOCAL_SUPPORTED) } } diff --git a/docs/ANNOUNCEMENT_GOSSIP.md b/docs/ANNOUNCEMENT_GOSSIP.md index 25593c6f..5bad0711 100644 --- a/docs/ANNOUNCEMENT_GOSSIP.md +++ b/docs/ANNOUNCEMENT_GOSSIP.md @@ -29,9 +29,13 @@ Existing TLVs (unchanged): Other optional extension: -- `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Bit 8 advertises - authenticated Noise private media (`PRIVATE_MEDIA_V1`); its exact value is - `00 01`. An empty value is valid and means no advertised capabilities. +- `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Assigned bits + match iOS: 0 prekeys, 1 Wi-Fi bulk, 2 gateway, 3 private groups, 4 board, + 5 vouch, 6 mesh diagnostics, 7 bridge, 8 authenticated Noise private media, + 9 private-media receipts, and 10 reserved non-destructive Noise replacement. + Bit 8's exact value is `00 01`. An empty value is valid and means no + advertised capabilities. Unknown bits and bytes beyond the low 64 bits must + be tolerated for forward compatibility. New TLV (optional): @@ -69,6 +73,9 @@ This matches the on‑wire 8‑byte `senderID`/`recipientID` encoding used in th security-sensitive bit is trusted only after the signed announcement key is bound to the matching remote static key from a live Noise handshake; see `PRIVATE_MEDIA_V1.md`. +- Capability bits are independent discovery hints. Implementations advertise + only wire features they implement and must not infer support from platform, + app version, or unrelated transports with similar names. - If a `0x04` TLV is present: - Interpret the value as `N = length / 8` peer IDs (ignore trailing non‑aligned bytes). - Each 8‑byte chunk is decoded back to a 16‑hex‑char peer ID string (lowercase). From 87ccfe3f6ca8804fa0fe892c8e57d1b84227ac97 Mon Sep 17 00:00:00 2001 From: Taksh Date: Sat, 15 Aug 2026 13:58:41 +0530 Subject: [PATCH 13/45] Use the shared packet identity for duplicate detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replay and duplicate detection keyed on a 32-bit contentHashCode over at most the first 64 bytes of the payload. Two packets from the same peer in the same millisecond that agreed on that prefix were the same packet as far as this cache was concerned, and a collision here is a dropped message: the second is discarded and nothing reports it. PacketIdUtil is the identity the rest of the stack already uses for this question — gossip sync membership, and the message IDs MessageHandler assigns — and iOS derives it identically: first 16 bytes of SHA-256 over type, senderID, timestamp and the whole payload. The security path now agrees with the sync path instead of carrying a weaker private notion of "same packet", and the FRAGMENT special case disappears because the full payload is covered either way. Peer scoping is deliberately kept. PacketIdUtil covers the packet's own senderID, which is not the peer it arrived from once relayed. --- .../bitchat/android/mesh/SecurityManager.kt | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 3f684679..7efa7fb8 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -3,6 +3,7 @@ package com.bitchat.android.mesh import android.util.Log import com.bitchat.android.crypto.EncryptionService import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.sync.PacketIdUtil import com.bitchat.android.protocol.MessageType import com.bitchat.android.model.RoutedPacket import com.bitchat.android.noise.AuthenticatedNoiseSession @@ -241,18 +242,28 @@ class SecurityManager(private val encryptionService: EncryptionService, private /** * Generate message ID for duplicate detection */ + /** + * Identity used for replay and duplicate detection. + * + * This was a 32-bit `contentHashCode()` over at most the first 64 bytes of + * the payload. Two packets from the same peer in the same millisecond that + * agreed on that prefix collided, and a collision here is a *dropped + * message* — the second packet is discarded as a duplicate and there is no + * signal that it happened. + * + * `PacketIdUtil` is the identity the rest of the stack already uses for + * exactly this question (gossip sync membership, message IDs in + * `MessageHandler`), and iOS derives it the same way: the first 16 bytes of + * SHA-256 over type, senderID, timestamp and the **whole** payload. Using + * it here makes the security path agree with the sync path instead of + * carrying a weaker private notion of "same packet". + * + * Peer scoping is kept: `PacketIdUtil` covers the packet's own senderID, + * while this key is scoped by the peer the packet was received from, and + * those are not the same thing for a relayed packet. + */ private fun generateMessageID(packet: BitchatPacket, peerID: String): String { - return when (MessageType.fromValue(packet.type)) { - MessageType.FRAGMENT -> { - // For fragments, include the payload hash to distinguish different fragments - "${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}" - } - else -> { - // For other messages, use a truncated payload hash - val payloadHash = packet.payload.sliceArray(0 until minOf(64, packet.payload.size)).contentHashCode() - "${packet.timestamp}-$peerID-$payloadHash" - } - } + return "$peerID-${PacketIdUtil.computeIdHex(packet)}" } /** From 4ef1b9c76576a7ddb5d857de17fcc3632bc39e5c Mon Sep 17 00:00:00 2001 From: Taksh Date: Sat, 15 Aug 2026 13:58:41 +0530 Subject: [PATCH 14/45] Pin the collision, the replay, and the peer scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collision case fails on main: two packets sharing a 64-byte prefix and a timestamp, where the second was silently dropped. The other two pass before and after on purpose. Replay of an identical packet must still be caught, and the same packet arriving from two different peers must still be tracked separately — strengthening the identity must not quietly weaken either. --- .../android/mesh/SecurityManagerTest.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt index 2a56e708..e43e11b5 100644 --- a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt @@ -598,4 +598,65 @@ class SecurityManagerTest { private fun String.hexToBytes(): ByteArray = chunked(2).map { it.toInt(16).toByte() }.toByteArray() + + // Duplicate-detection identity. + + /** + * Two distinct packets that agree on their first 64 bytes and share a + * timestamp. The old key hashed only that prefix, with a 32-bit + * `contentHashCode`, so these were "the same packet" and the second was + * silently dropped. + */ + private fun prefixSharingPair(): Pair { + val shared = ByteArray(64) { 0x7 } + val first = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = otherPeerID.hexToByteArrayForTest(), + recipientID = myPeerID.hexToByteArrayForTest(), + timestamp = 1_700_000_000_000uL, + payload = shared + byteArrayOf(0x01, 0x02, 0x03), + ttl = 7u + ) + val second = first.copy(payload = shared + byteArrayOf(0x0A, 0x0B, 0x0C)) + return first to second + } + + @Test + fun `packets differing only past the first 64 bytes are not treated as duplicates`() { + val (first, second) = prefixSharingPair() + + assertTrue(securityManager.validatePacket(first, otherPeerID)) + assertTrue( + "A distinct packet must not be dropped as a duplicate", + securityManager.validatePacket(second, otherPeerID) + ) + } + + @Test + fun `a genuine replay of the same packet is still rejected`() { + // The other half: strengthening the identity must not weaken replay + // protection, which is the reason this cache exists. + val (first, _) = prefixSharingPair() + + assertTrue(securityManager.validatePacket(first, otherPeerID)) + assertFalse( + "The identical packet must still be caught", + securityManager.validatePacket(first, otherPeerID) + ) + } + + @Test + fun `the same packet from two different peers is tracked separately`() { + // Peer scoping is deliberately kept: PacketIdUtil covers the packet's + // own senderID, which is not the peer it was received from once a + // packet has been relayed. + val (first, _) = prefixSharingPair() + + assertTrue(securityManager.validatePacket(first, otherPeerID)) + assertTrue(securityManager.validatePacket(first, unknownPeerID)) + } + + private fun String.hexToByteArrayForTest(): ByteArray = + chunked(2).map { it.toInt(16).toByte() }.toByteArray() } From b028270cc40bbea42591c8541bfa290c1c12b248 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 16 Aug 2026 06:08:32 +0000 Subject: [PATCH 15/45] Automated update of relay data - Sun Aug 16 06:08:32 UTC 2026 --- app/src/main/assets/nostr_relays.csv | 802 ++++++++++++++------------- 1 file changed, 402 insertions(+), 400 deletions(-) diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index 324c0df1..0d46c11e 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,414 +1,416 @@ Relay URL,Latitude,Longitude -nostr.n7ekb.net,47.4941,-122.294 -relay.kilombino.com,43.6532,-79.3832 -relay02.lnfi.network,35.6764,139.65 -relay.liberbitworld.org,43.6532,-79.3832 -articles.layer3.news,37.3387,-121.885 -relay.nostr.net,43.6532,-79.3832 -nostr.easycryptosend.it,43.6532,-79.3832 -strfry.bonsai.com,39.0438,-77.4874 -relay.olas.app:443,60.1699,24.9384 -relay.sharegap.net,43.6532,-79.3832 -relay.ditto.pub,43.6532,-79.3832 -nostrride.io,37.3986,-121.964 -relay.favillakey.io,-27.4705,153.026 -nostr-relay.xbytez.io,50.6924,3.20113 -relay.nostrmap.net:443,60.1699,24.9384 -mostro-p2p.tech,50.1109,8.68213 -relay.aidatanorge.no,43.6532,-79.3832 -nostr.openhoofd.nl,51.5717,3.70417 -relay.chatbett.de,40.7128,-74.006 -relay.ohstr.com:443,43.6532,-79.3832 -nostr.overmind.lol:443,43.6532,-79.3832 -nostr.red5d.dev,43.6532,-79.3832 -relay.snort.social,53.3498,-6.26031 -relay.directsponsor.net,42.8864,-78.8784 -relay.bullishbounty.com:443,43.6532,-79.3832 -relay.scuba323.com,40.8218,-74.45 -nostr.spicyz.io,43.6532,-79.3832 -nostr.chaima.info,50.1109,8.68213 -nostr.purpura.cloud,43.6532,-79.3832 -relay.nostrops.com,32.71,-96.6745 -relay.veganostr.com:443,60.1699,24.9384 -relay.ohstr.com,43.6532,-79.3832 -soloco.nl,43.6532,-79.3832 -nostr.plantroon.com:443,50.1013,8.62643 -relay.chorus.community:443,48.5333,10.7 -relay.fiatdenier.com,43.7787,-79.5393 -nostr.whitenode45.ddns.net,40.55,-74.4758 -relay.cosmicbolt.net:443,37.3986,-121.964 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -relay.cosmicbolt.net,37.3986,-121.964 -fanfares.nostr1.com:443,40.7057,-74.0136 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -relay.keykeeper.world,40.7824,-74.0711 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -dm-test-strfry-generic.samt.st,43.6532,-79.3832 -relay.laantungir.net,-19.4692,-42.5315 -nostr.sathoarder.com,48.5734,7.75211 -relay.mccormick.cx:443,52.3563,4.95714 -relay.solife.me,43.6532,-79.3832 -relay.fountain.fm:443,43.6532,-79.3832 -nostrcity-club.fly.dev,38.7946,-106.535 -nostr.mas-family.eu,60.3478,15.7505 -no.str.cr,8.96171,-83.5246 -relay.wavefunc.live,41.8781,-87.6298 -relay.cyberguy.fyi,52.6907,4.8181 -adre.su,59.9311,30.3609 -relay.erybody.com,41.4513,-81.7021 -relay.mrmave.work,43.6532,-79.3832 -relay.decentralia.fr,48.122,11.589 -relay.mccormick.cx,52.3563,4.95714 -nostr.dlcdevkit.com:443,40.0992,-83.1141 -nostr-02.yakihonne.com,1.32123,103.695 -nostr.21crypto.ch,47.5356,8.73209 -slick.mjex.me,39.0418,-77.4744 -nostr.spicyz.io:443,43.6532,-79.3832 -relay-dev.gulugulu.moe,43.6532,-79.3832 -relay.nostx.io,43.6532,-79.3832 -relay.wisp.talk,49.4543,11.0746 -relay.plebeian.market:443,50.1109,8.68213 -relay.mostr.pub:443,43.6532,-79.3832 -relay.aarpia.com,37.3986,-121.964 -chat-relay.zap-work.com,43.6532,-79.3832 -chat-relay.zap-work.com:443,43.6532,-79.3832 -relay.mitchelltribe.com:443,39.0438,-77.4874 -relay.paulstephenborile.com,49.4543,11.0746 -relay.trotters.cc,43.6532,-79.3832 -nexus.libernet.app:443,43.6532,-79.3832 -syb.lol,34.0549,-118.243 -relay.cypherflow.ai,48.8575,2.35138 -relay.despera.space,52.1782,20.9926 -relay.loveisbitcoin.com,43.6532,-79.3832 -relay.rushmi0.win,43.6532,-79.3832 -relay.degmods.com,50.4754,12.3683 -relay.satlantis.io,39.0438,-77.4874 -relay.underorion.se,50.1109,8.68213 -nostr.mom:443,50.4754,12.3683 -nostr-verified.wellorder.net,45.5201,-122.99 -relay.nostreon.com,60.1699,24.9384 -nostr.christiansass.de,54.3213,10.1349 -rilo.nostria.app:443,43.6532,-79.3832 -relay.trotters.cc:443,43.6532,-79.3832 -kotukonostr.onrender.com,37.7775,-122.397 -relay.nmail.li,50.9871,2.12554 -relay.artio.inf.unibe.ch,46.9501,7.43678 -nos.lol,50.4754,12.3683 -nostr.snowbla.de:443,50.4754,12.3683 -relay.openresist.com,43.6532,-79.3832 -ribo.us.nostria.app,43.6532,-79.3832 -bitchat.nostr1.com,40.7057,-74.0136 -vm-1734.lnvps.cloud,53.3498,-6.26031 -nostr-relay.xbytez.io:443,50.6924,3.20113 -nostr.sathoarder.com:443,48.5734,7.75211 -bcast.girino.org,43.6532,-79.3832 -ribo.us.nostria.app:443,43.6532,-79.3832 -strfry.shock.network,39.0438,-77.4874 -relay.staging.plebeian.market:443,51.5072,-0.127586 -offchain.pub:443,39.1585,-94.5728 -relayone.soundhsa.com,39.0997,-94.5786 -relay.libernet.app,43.6532,-79.3832 -relay.earthly.city,34.1749,-118.54 -relayrs.notoshi.win:443,43.6532,-79.3832 -nostr.vulpem.com,49.4543,11.0746 -relay.getsafebox.app:443,43.6532,-79.3832 -relay.paulstephenborile.com:443,49.4543,11.0746 -nostr.unkn0wn.world,46.8499,9.53287 -relay.binaryrobot.com,43.6532,-79.3832 -relay.wellorder.net,45.5201,-122.99 -relayone.geektank.ai:443,39.0997,-94.5786 -relay.wasabiwallet.io,43.6532,-79.3832 -top.testrelay.top,43.6532,-79.3832 -nostr.2b9t.xyz,34.0549,-118.243 -relay.nostr.com,50.1109,8.68213 -nostr.azzamo.net:443,52.2633,21.0283 -nostr.dlcdevkit.com,40.0992,-83.1141 -nostr.robosats.org,64.1476,-21.9392 -nostr.iskarion.ddns.net,43.3076,-2.95421 -relay2.angor.io,48.1046,11.6002 -relay.mmwaves.de,48.8575,2.35138 -relay.agorist.space:443,52.3734,4.89406 -relay.dreamith.to,43.6532,-79.3832 -nostr-02.uid.ovh,50.9871,2.12554 -nostr.thebiglake.org,32.71,-96.6745 -nostr-relay.corb.net,39.6478,-104.988 -relay.dyne.org,49.0291,8.35705 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -relay.angor.io,48.1046,11.6002 -nostrelay.circum.space,52.2245,8.826 -nostr-01.uid.ovh,50.9871,2.12554 -maxq.descendant.io,43.6532,-79.3832 -offchain.bostr.online,43.6532,-79.3832 -relay.binaryrobot.com:443,43.6532,-79.3832 -nostr.planix.org,43.6532,-79.3832 -tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 -prl.plus,55.7628,37.5983 -relay.mwaters.net,50.9871,2.12554 -dev.relay.stream,43.6532,-79.3832 -relay.bornheimer.app,50.1109,8.68213 -inbox.scuba323.com,40.8218,-74.45 -relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -relay.nuts.cash,52.3676,4.90414 -bruh.samt.st,43.6532,-79.3832 -nostr.carroarmato0.be:443,50.914,3.21378 -relay.plebeian.market,50.1109,8.68213 -schnorr.me:443,43.6532,-79.3832 -nostr.thalheim.io:443,60.1699,24.9384 -nostr.88mph.life,52.1941,-2.21905 -cache.trustr.ing,43.6548,-79.3885 -ec2.f7z.io,60.1699,24.9384 -relay.btcforplebs.com,43.6532,-79.3832 -nostr.liberty.fans,36.8767,-89.5879 -syb.lol:443,34.0549,-118.243 -nostr.islandarea.net:443,35.4669,-97.6473 -nostr.mom,50.4754,12.3683 -nostr.data.haus:443,50.4754,12.3683 -strfry.bonsai.com:443,39.0438,-77.4874 -nostr.highway15.net,28.0445,-82.6699 -relay.wavlake.com:443,41.2619,-95.8608 -relay.nostriot.com:443,41.5695,-83.9786 -relay.gulugulu.moe:443,43.6532,-79.3832 -rele.speyhard.fi,50.1109,8.68213 -nostr.hoppe-relay.it.com,42.8864,-78.8784 -nostr2.girino.org:443,43.6532,-79.3832 -nostr.oxtr.dev,50.4754,12.3683 -nostr.bitcoiner.social:443,40.7608,-111.891 -nostrrelay.taylorperron.com,45.5029,-73.5723 -nostr-01.yakihonne.com:443,1.32123,103.695 -nostr.tagomago.me,42.3601,-71.0589 -relay.hivetalk.org,40.8302,-74.1299 -relay.sigit.io:443,50.4754,12.3683 -relay.openresist.com:443,43.6532,-79.3832 -cs-relay.nostrdev.com:443,50.4754,12.3683 -no.str.cr:443,8.96171,-83.5246 -relay.nearhood.co.uk,51.5134,-0.0890675 -relay.lanavault.space,60.1699,24.9384 -relay.guggero.org,46.5971,9.59652 -nexus.libernet.app,43.6532,-79.3832 -relay.nostriot.com,41.5695,-83.9786 -dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 -x.kojira.io:443,43.6532,-79.3832 -nostr.4rs.nl,49.0291,8.35696 -relayone.geektank.ai,39.0997,-94.5786 -nostr.debate.report,50.1109,8.68213 -relay.gulugulu.moe,43.6532,-79.3832 -public.crostr.com,43.6532,-79.3832 -offchain.pub,39.1585,-94.5728 -relay.tdw.lol,41.8781,-87.6298 -relay5.bitransfer.org,43.6532,-79.3832 -relay.manneken.brussels,49.4543,11.0746 -chorus.pjv.me,45.5201,-122.99 -relay.fckstate.net,59.3293,18.0686 -treuzkas.branruz.com,48.8575,2.35138 -nostr-relay.cbrx.io,43.6532,-79.3832 -nostr-relay.nextblockvending.com,47.2343,-119.853 -nostr.fullstackcash.net,45.5201,-122.99 -relay.primal.net,43.6532,-79.3832 -relay.mappingbitcoin.com,43.6532,-79.3832 -nostr.mikoshi.de,50.1109,8.68213 -nostr.stakey.net,52.3676,4.90414 -nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397 -relay.wavlake.com,41.2619,-95.8608 -relay.ordoplay.com,50.1109,8.68213 -relay.klabo.world,47.674,-122.122 -buzz.cashu.space,50.1109,8.68213 -relay.nostrhub.fr,48.1045,11.6004 -portal-relay.pareto.space,49.0291,8.35696 -dev-relay.nostreon.com,60.1699,24.9384 -relay.islandbitcoin.com,12.8498,77.6545 -testr.nymble.world,40.8054,-74.0241 -relayrs.notoshi.win,43.6532,-79.3832 -nostr-relay.corb.net:443,39.6478,-104.988 -cdn.satellite.earth,40.8302,-74.1299 -rilo.nostria.app,43.6532,-79.3832 -nostr.pbfs.io,50.4754,12.3683 -nostr2.girino.org,43.6532,-79.3832 -0x-nostr-relay.fly.dev,38.7946,-106.535 -nostr.girino.org,43.6532,-79.3832 -relay.layer.systems,49.0291,8.35695 -relay.olas.app,60.1699,24.9384 -ribo.eu.nostria.app,43.6532,-79.3832 -buzz.ac2n-share.kozow.com,45.764,4.83566 -auth.nostr1.com,40.7057,-74.0136 -relay.bitmacro.cloud,43.6532,-79.3832 -relay.qstr.app,50.1109,8.68213 -relay.ru.ac.th,13.7607,100.627 -ribo.nostria.app:443,43.6532,-79.3832 -relay.44billion.net,43.6532,-79.3832 -cs-relay.nostrdev.com,50.4754,12.3683 -relay.nostrian-conquest.com:443,41.223,-111.974 -relay.bullishbounty.com,43.6532,-79.3832 -relay.edufeed.org,49.4521,11.0767 -porchlight.social,43.6532,-79.3832 nostr.bitcoiner.social,40.7608,-111.891 -testnet-relay.samt.st,40.8302,-74.1299 -relay.vrtmrz.net,43.6532,-79.3832 -nostr.wecsats.io:443,43.6532,-79.3832 -relay.satmaxt.xyz:443,43.6532,-79.3832 -nostr.snowbla.de,50.4754,12.3683 -nostr.rtvslawenia.com:443,49.4543,11.0746 -relay.flashapp.me,43.6548,-79.3885 -purplerelay.com:443,43.6532,-79.3832 -relay.staging.plebeian.market,51.5072,-0.127586 -relay.libernet.app:443,43.6532,-79.3832 -relay.0xchat.com,43.6532,-79.3832 -myvoiceourstory.org,37.3598,-121.981 -nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 -relay.vrtmrz.net:443,43.6532,-79.3832 -relay.zone667.com,60.1699,24.9384 -nostr.nodesmap.com,59.3327,18.0656 +relay.mostr.pub,43.6532,-79.3832 +relay.veganostr.com:443,60.1699,24.9384 +relay.dreamith.to,43.6532,-79.3832 +buzz.cashu.space,50.1109,8.68213 +relay.sincensura.org,43.6532,-79.3832 +relayrs.notoshi.win:443,43.6532,-79.3832 +nostr.bond,50.1109,8.68213 +relay.edufeed.org,49.4521,11.0767 +relay.bullishbounty.com:443,43.6532,-79.3832 +relay.nostr.blockhenge.com,39.0438,-77.4874 +relay2.fiatdenier.com,50.1013,8.62643 +relay.mwaters.net,50.9871,2.12554 +relay2.angor.io,48.1046,11.6002 +memlay.v0l.io,53.3498,-6.26031 +nostr.wecsats.io,43.6532,-79.3832 +vm-1734.lnvps.cloud,53.3498,-6.26031 +dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 +nostr2.girino.org:443,43.6532,-79.3832 +nostr.robosats.org,64.1476,-21.9392 +relay.atsocy.com,43.6532,-79.3832 +relayone.geektank.ai:443,39.0997,-94.5786 +relay.olas.app:443,60.1699,24.9384 +relay.nostu.be,40.4167,-3.70329 +relay.otrta.me,50.1109,8.68213 +nostr.spaceshell.xyz,43.6532,-79.3832 +rilo.nostria.app,43.6532,-79.3832 relay.agorist.space,52.3734,4.89406 -relayone.soundhsa.com:443,39.0997,-94.5786 -relay.satmaxt.xyz,43.6532,-79.3832 -relay.zapanimations.com,40.8054,-74.0241 -relay.sovereignresonance.org,48.8575,2.35138 -nostr.relay.hedwig.sh,60.1699,24.9384 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -relay.arx-ccn.com,50.4754,12.3683 -nip85.nosfabrica.com,39.0997,-94.5786 +relay.nostrmap.net,60.1699,24.9384 +relay.bullishbounty.com,43.6532,-79.3832 nrs-01.darkcloudarcade.com,39.0997,-94.5786 -spamspamspamspam.rest,43.6532,-79.3832 -nostr.davenov.com,50.1109,8.68213 -relay.littlebitstudios.com,43.6532,-79.3832 -purplerelay.com,43.6532,-79.3832 -nos.lol:443,50.4754,12.3683 -relay2.veganostr.com,60.1699,24.9384 -relay-rpi.edufeed.org,49.4521,11.0767 -relay.lacrypta.ar,43.6532,-79.3832 -relay.tdw.gg,34.0549,-118.243 -relay.getvia.xyz,60.1699,24.9384 -relay.lanacoin-eternity.com,40.8302,-74.1299 -conduitl2.fly.dev,38.7946,-106.535 +relay.angor.io:443,48.1046,11.6002 nostr.rtvslawenia.com,49.4543,11.0746 -relay.wavefunc.live:443,41.8781,-87.6298 +relay.wasabiwallet.io,43.6532,-79.3832 +cs-relay.nostrdev.com:443,50.4754,12.3683 +nostr.4rs.nl,49.0291,8.35696 +nexus.libernet.app,43.6532,-79.3832 +relay-dev.gulugulu.moe,43.6532,-79.3832 +nostr.88mph.life,52.1941,-2.21905 +relayone.soundhsa.com:443,39.0997,-94.5786 +social.amanah.eblessing.co,48.1046,11.6002 +relay.getsafebox.app,43.6532,-79.3832 +relay.44billion.net,43.6532,-79.3832 +relay.conduit.market,38.7946,-106.535 +nostr.infero.net,35.6764,139.65 +relay.staging.plebeian.market,51.5072,-0.127586 +relay.snort.social,53.3498,-6.26031 +purplerelay.com:443,43.6532,-79.3832 +nostr.mas-family.eu,60.3478,15.7505 +relay.ru.ac.th,13.7607,100.627 +relay.openresist.com,43.6532,-79.3832 +antiprimal.net,43.6532,-79.3832 +relay.illuminodes.com,43.6532,-79.3832 +relay.mmwaves.de:443,48.8575,2.35138 +nostr.spicyz.io,43.6532,-79.3832 +relay.nostrfeed.com,60.1699,24.9384 +strfry.apps3.slidestr.net,40.4167,-3.70329 +relay.directsponsor.net,42.8864,-78.8784 +relay.nostrhub.fr,48.1045,11.6004 +nostr.plantroon.com,50.1013,8.62643 +nostr-verified.wellorder.net,45.5201,-122.99 +relay.bnos.space:443,43.6532,-79.3832 +relay.pyramid.li,47.4093,8.46503 +public.crostr.com,43.6532,-79.3832 +relay.kilombino.com,43.6532,-79.3832 +relay.libernet.app:443,43.6532,-79.3832 +relay.internationalright-wing.org,-22.4692,-48.9875 +nostr.overmind.lol:443,43.6532,-79.3832 +relay.ordoplay.com,50.1109,8.68213 +mostro-p2p.tech,50.1109,8.68213 +offchain.pub:443,39.1585,-94.5728 +cs-relay.nostrdev.com,50.4754,12.3683 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +nostr.unkn0wn.world,46.8499,9.53287 +relay.shadowbip.com,50.1109,8.68213 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +relay.staging.commonshub.brussels,49.4543,11.0746 +relay2.angor.io:443,48.1046,11.6002 +relay.arx-ccn.com,50.4754,12.3683 +relay.underorion.se,50.1109,8.68213 +bread.nostrsms.com,40.8218,-74.45 +relay.cypherflow.ai,48.8575,2.35138 +nostr.islandarea.net:443,35.4669,-97.6473 +relay.homeinhk.xyz,35.694,139.754 +relay.ditto.pub:443,43.6532,-79.3832 +nostr.fullstackcash.net,45.5201,-122.99 +bitcoinostr.duckdns.org,38.9504,-0.14007 +relay.trotters.cc,43.6532,-79.3832 +portal-relay.pareto.space,49.0291,8.35696 +relay.openresist.com:443,43.6532,-79.3832 +relay.endfiat.money,59.3327,18.0656 +nostr.hoppe-relay.it.com,42.8864,-78.8784 +nostr.sovereignservices.xyz,43.6532,-79.3832 +relay.fundstr.me,42.3601,-71.0589 +relay5.bitransfer.org,43.6532,-79.3832 +nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 +testr.nymble.world,40.8054,-74.0241 +strfry.shock.network:443,39.0438,-77.4874 +chorus.mikedilger.com:444,-36.8906,174.794 +relay.minibolt.info,43.6532,-79.3832 +nos.lol:443,50.4754,12.3683 +relay.liberbitworld.org,43.6532,-79.3832 +maxq.descendant.io,43.6532,-79.3832 +nostrcity-club.fly.dev,38.7946,-106.535 +relay.nostriot.com,41.5695,-83.9786 +nostr.n7ekb.net,47.4941,-122.294 +relay.erybody.com,41.4513,-81.7021 +nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397 +nostrrelay.taylorperron.com,45.5029,-73.5723 +syb.lol,34.0549,-118.243 +relay.nostr.com,50.1109,8.68213 +relay.hivetalk.org,40.8302,-74.1299 +relay.degmods.com,50.4754,12.3683 +relay.laantungir.net,-19.4692,-42.5315 +relay.nearhood.co.uk,51.5134,-0.0890675 +relay.tdw.lol,41.8781,-87.6298 +ribo.us.nostria.app,43.6532,-79.3832 +public.obelisk.ar,43.6532,-79.3832 +prl.plus,55.7628,37.5983 +21milionidinostr.duckdns.org,45.5192,9.08625 nostr.carroarmato0.be,50.914,3.21378 nostr.pbfs.io:443,50.4754,12.3683 -ribo.nostria.app,43.6532,-79.3832 -nostr.sovereignservices.xyz,43.6532,-79.3832 -relay.mostr.pub,43.6532,-79.3832 -schnorr.me,43.6532,-79.3832 -x.kojira.io,43.6532,-79.3832 -public.obelisk.ar,43.6532,-79.3832 -relay.angor.io:443,48.1046,11.6002 -nostr.oxtr.dev:443,50.4754,12.3683 -nostr.ltd,19.076,72.8777 -nostr.tac.lol:443,47.4748,-122.273 -nostr.twinkle.lol,51.902,7.6657 -relay.s-w.art,43.6532,-79.3832 -relay.openfarmtools.org,60.1699,24.9384 -nostr.hekster.org,37.3986,-121.964 -strfry.apps3.slidestr.net,40.4167,-3.70329 -relay.mypathtofire.de,42.8864,-78.8784 -nostr.wecsats.io,43.6532,-79.3832 -nostr.computingcache.com,34.0356,-118.442 -nostr.janx.com,43.6532,-79.3832 -bread.nostrsms.com,40.8218,-74.45 -relay.chorus.community,48.5333,10.7 -testnet-relay.samt.st:443,40.8302,-74.1299 -relay.kaleidoswap.com,50.8476,4.35717 -yabu.me,35.6092,139.73 -nostr-dev.wellorder.net,45.5201,-122.99 -relay.layer.systems:443,49.0291,8.35695 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 -nostr-relay.zimage.com,34.282,-118.439 -relay.fundstr.me,42.3601,-71.0589 -nostr.stakey.net:443,52.3676,4.90414 -relay.satsmarkt.club,52.6907,4.8181 -relay.satsapp.me,50.4754,12.3683 -bendernostur.duckdns.org:8443,50.1109,8.68213 -relay.illuminodes.com,43.6532,-79.3832 -nostr.chaima.info:443,50.1109,8.68213 -nostr.hekster.org:443,37.3986,-121.964 -nostr.yutakobayashi.com,43.6532,-79.3832 -freelay.sovbit.host,60.1699,24.9384 -insta-relay.apps3.slidestr.net,40.4167,-3.70329 -infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 -relay.internationalright-wing.org,-22.4692,-48.9875 -relay.mmwaves.de:443,48.8575,2.35138 public.crostr.com:443,43.6532,-79.3832 -relay.nostrian-conquest.com,41.223,-111.974 -relay-rpi.edufeed.org:443,49.4521,11.0767 -relay2.angor.io:443,48.1046,11.6002 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -bunk-test.feeds.relay.tools,38.6327,-90.1961 -nostr-pr02.redscrypt.org,52.3676,4.90414 -nostr.tac.lol,47.4748,-122.273 -nostr.overpay.com,29.7449,-95.5343 -relay.novospes.com,43.6532,-79.3832 -nostrcity-club.fly.dev:443,38.7946,-106.535 -relay01.lnfi.network,35.6764,139.65 -relay.lightning.pub:443,39.0438,-77.4874 -relay.pyramid.li,47.4093,8.46503 -relay.nostu.be,40.4167,-3.70329 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -relay.endfiat.money:443,59.3327,18.0656 -nostr.spaceshell.xyz,43.6532,-79.3832 -21milionidinostr.duckdns.org,45.5192,9.08625 -articles.layer3.news:443,37.3387,-121.885 -wheat.happytavern.co,43.6532,-79.3832 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -nostr.na.social,43.6532,-79.3832 -social.amanah.eblessing.co,48.1046,11.6002 -reraw.pbla2fish.cc,43.6532,-79.3832 -nostr.girino.org:443,43.6532,-79.3832 -staging.yabu.me,35.6092,139.73 -nostr.islandarea.net,35.4669,-97.6473 -relay.endfiat.money,59.3327,18.0656 -relay.mitchelltribe.com,39.0438,-77.4874 -nostr.myshosholoza.co.za,52.3676,4.90414 -relay.staging.commonshub.brussels,49.4543,11.0746 +espelho.girino.org,43.6532,-79.3832 +nostr.computingcache.com,45.5341,-122.956 +relay.chorus.community:443,48.5333,10.7 +nostr-relay.nextblockvending.com,47.2343,-119.853 +nostr.easycryptosend.it,43.6532,-79.3832 +no.str.cr:443,8.96171,-83.5246 +nostr.snowbla.de:443,50.4754,12.3683 bridge.tagomago.me,42.3601,-71.0589 -relay.agentry.com,42.8864,-78.8784 -testnet.samt.st,43.6532,-79.3832 -nostr.wild-vibes.ts.net,48.8566,2.35222 -relay.getsafebox.app,43.6532,-79.3832 -relay.notoshi.win,13.3396,100.93 -bitcoinostr.duckdns.org,41.1976,1.11167 -relay.sigit.io,50.4754,12.3683 -bucket.coracle.social,37.7775,-122.397 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -vault.iris.to:443,43.6532,-79.3832 -relay.wisp.talk:443,49.4543,11.0746 -relay.mostro.network,40.8302,-74.1299 -nostr.l484.com,30.2672,-97.7431 -relay.froth.zone,60.1699,24.9384 -relay.beamhop.com,43.6532,-79.3832 -relay.sincensura.org,43.6532,-79.3832 -relay.ditto.pub:443,43.6532,-79.3832 -nostr.azzamo.net,52.2633,21.0283 -relay.edufeed.org:443,49.4521,11.0767 -ribo.eu.nostria.app:443,43.6532,-79.3832 -vault.iris.to,43.6532,-79.3832 -nostr.plantroon.com,50.1013,8.62643 -fanfares.nostr1.com,40.7057,-74.0136 -budabit.nostr1.com,40.7057,-74.0136 -relay.damus.io,43.6532,-79.3832 -strfry.shock.network:443,39.0438,-77.4874 -bitcoiner.social,40.7608,-111.891 -basspistol.org,49.0291,8.35696 -nostr.thalheim.io,60.1699,24.9384 -relay.conduit.market,38.7946,-106.535 -relay.yoinekodo.jp,43.6532,-79.3832 -nostrbtc.com,43.6532,-79.3832 -relay.lightning.pub,39.0438,-77.4874 -nostr-01.yakihonne.com,1.32123,103.695 -nostr-pub.wellorder.net,45.5201,-122.99 +relay.endfiat.money:443,59.3327,18.0656 +relay.klabo.world,47.674,-122.122 +articles.layer3.news:443,37.3387,-121.885 +nostr.21crypto.ch,47.5356,8.73209 +relay.kaleidoswap.com,50.8476,4.35717 +nostr.relay.hedwig.sh,60.1699,24.9384 +nostr.yutakobayashi.com,43.6532,-79.3832 strfry.ymir.cloud,43.6532,-79.3832 -relay.minibolt.info,43.6532,-79.3832 -nostr.data.haus,50.4754,12.3683 -relay.otrta.me,50.1109,8.68213 -relay.samt.st,40.8302,-74.1299 -relay.nostr.blockhenge.com,39.0438,-77.4874 -relay.minibolt.info:443,43.6532,-79.3832 -nostr.2b9t.xyz:443,34.0549,-118.243 +relay.littlebitstudios.com,43.6532,-79.3832 +testnet-relay.samt.st:443,40.8302,-74.1299 +relay.islandbitcoin.com,12.8498,77.6545 +relay.zone667.com,60.1699,24.9384 +relay.lightning.pub:443,39.0438,-77.4874 +relay.layer.systems:443,49.0291,8.35695 +offchain.pub,39.1585,-94.5728 +nos.lol,50.4754,12.3683 +cdn.satellite.earth,40.8302,-74.1299 +relay-us.zombi.cloudrodion.com,40.7862,-74.0743 +relay.wisp.talk:443,49.4543,11.0746 +nostr.hekster.org:443,37.3986,-121.964 +nostr.mom:443,50.4754,12.3683 +relay.mccormick.cx:443,52.3563,4.95714 +dev.relay.stream,43.6532,-79.3832 dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -relay.veganostr.com,60.1699,24.9384 -relay.fountain.fm,43.6532,-79.3832 +relay.vrtmrz.net:443,43.6532,-79.3832 +relayone.soundhsa.com,39.0997,-94.5786 +relay.agorist.space:443,52.3734,4.89406 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +nostr.dlcdevkit.com:443,40.0992,-83.1141 node.kommonzenze.de,49.4521,11.0767 -relay-arg.zombi.cloudrodion.com,1.35208,103.82 -nostr.myshosholoza.co.za:443,52.3676,4.90414 -temp.iris.to,43.6532,-79.3832 -relay.nostrmap.net,60.1699,24.9384 +relay.beamhop.com,43.6532,-79.3832 +nostr.2b9t.xyz:443,34.0549,-118.243 +relay.chatbett.de,40.7128,-74.006 +relay.tdw.gg,34.0549,-118.243 +budabit.nostr1.com,40.7057,-74.0136 +nostr.novacisko.cz,52.2026,20.9397 +relay.samt.st,40.8302,-74.1299 +bruh.samt.st,43.6532,-79.3832 +nostr.wild-vibes.ts.net,48.8566,2.35222 +relay.opossumwire.org,34.0549,-118.243 +relay.angor.io,48.1046,11.6002 +relay.nostrmap.net:443,60.1699,24.9384 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +chat-relay.zap-work.com,43.6532,-79.3832 +relay.earthly.city,34.1749,-118.54 +relay.mappingbitcoin.com,43.6532,-79.3832 +relay.satmaxt.xyz,43.6532,-79.3832 +relay.primal.net,43.6532,-79.3832 +relay.bornheimer.app,50.1109,8.68213 +nostr.tagomago.me,42.3601,-71.0589 relay.orly.dev,32.7767,-96.797 +no.str.cr,8.96171,-83.5246 +auth.nostr1.com,40.7057,-74.0136 +relay.opmaat.org,60.1699,24.9384 +temp.iris.to,43.6532,-79.3832 +conduitl2.fly.dev,38.7946,-106.535 +yabu.me,35.6092,139.73 +nostrride.io,37.3986,-121.964 +nostr.plantroon.com:443,50.1013,8.62643 +relay.wavlake.com,41.2619,-95.8608 +nostrbtc.com,43.6532,-79.3832 +relay.mitchelltribe.com:443,39.0438,-77.4874 +relay.manneken.brussels,49.4543,11.0746 +relay.lanacoin-eternity.com,40.8302,-74.1299 +bendernostur.duckdns.org:8443,50.1109,8.68213 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +nostr.nodesmap.com,59.3327,18.0656 +nostr-01.yakihonne.com,1.32123,103.695 +bunk-test.feeds.relay.tools,38.6327,-90.1961 +relay.btcforplebs.com,43.6532,-79.3832 +relay.beginningend.com,35.2227,-97.4786 +nostr.iskarion.ddns.net,43.3076,-2.95421 +nostr-relay.cbrx.io,43.6532,-79.3832 +relay.nostrian-conquest.com,41.223,-111.974 +relay.cosmicbolt.net:443,37.3986,-121.964 +nostr.oxtr.dev:443,50.4754,12.3683 +nostr-relay.acloud.kdns.fr,43.6532,-79.3832 +relay.mccormick.cx,52.3563,4.95714 +relay.fiatdenier.com,43.7787,-79.5393 +nostr.bitcoiner.social:443,40.7608,-111.891 +relay.gulugulu.moe:443,43.6532,-79.3832 +relay.piazza.today,48.122,11.589 +nostr.ac,38.958,-77.3592 +strfry.bonsai.com,39.0438,-77.4874 +dev-relay.nostreon.com,60.1699,24.9384 +relay.olas.app,60.1699,24.9384 +relay.loveisbitcoin.com,43.6532,-79.3832 +strfry.bonsai.com:443,39.0438,-77.4874 +nostr.tac.lol,47.4748,-122.273 +rilo.nostria.app:443,43.6532,-79.3832 +nostr.azzamo.net,52.2633,21.0283 +nostr.wecsats.io:443,43.6532,-79.3832 +relay.wellorder.net,45.5201,-122.99 +relay.libernet.app,43.6532,-79.3832 +nostr.thalheim.io,60.1699,24.9384 +relayrs.notoshi.win,43.6532,-79.3832 +ribo.nostria.app:443,43.6532,-79.3832 +relay.gulugulu.moe,43.6532,-79.3832 +relay.qstr.app,50.1109,8.68213 +myvoiceourstory.org,37.3598,-121.981 +nostr-relay.zimage.com,34.282,-118.439 +treuzkas.branruz.com,48.8575,2.35138 +top.testrelay.top,43.6532,-79.3832 +relay.cyberguy.fyi,52.6907,4.8181 +articles.layer3.news,37.3387,-121.885 +nostr.sathoarder.com,48.5734,7.75211 +nostr.carroarmato0.be:443,50.914,3.21378 +nostr-02.uid.ovh,50.9871,2.12554 +ribo.us.nostria.app:443,43.6532,-79.3832 +relay.ditto.pub,43.6532,-79.3832 +nostr.chaima.info:443,50.1109,8.68213 +relay.bnos.space,43.6532,-79.3832 +relay.solife.me,43.6532,-79.3832 +relay.wavlake.com:443,41.2619,-95.8608 +bucket.coracle.social,37.7775,-122.397 +nostr.christiansass.de,52.5244,13.4105 +relay.dyne.org,49.0291,8.35705 +testnet-relay.samt.st,40.8302,-74.1299 +nostream.seitendan.com,34.7062,135.493 +basspistol.org,49.0291,8.35696 +purplerelay.com,43.6532,-79.3832 +relay.wisp.talk,49.4543,11.0746 +relay.getsafebox.app:443,43.6532,-79.3832 +freelay.sovbit.host,60.1699,24.9384 +relay.sigit.io:443,50.4754,12.3683 +buzz.ac2n-share.kozow.com,45.764,4.83566 +relay.mostro.network,40.8302,-74.1299 +nostrcheck.me,43.6532,-79.3832 +nostr.rtvslawenia.com:443,49.4543,11.0746 +insta-relay.apps3.slidestr.net,40.4167,-3.70329 +nostr.vulpem.com,49.4543,11.0746 +relay-na1.metanomalist.com,43.6532,-79.3832 +chorus.bonsai.com,39.0438,-77.4874 +ec2.f7z.io,60.1699,24.9384 +relay-dev.gulugulu.moe:443,43.6532,-79.3832 +nostr.twinkle.lol,51.902,7.6657 +relay.lanavault.space,60.1699,24.9384 +nostr.pbfs.io,50.4754,12.3683 +bitchat.nostr1.com,40.7057,-74.0136 +nostr.highway15.net,28.0445,-82.6699 +nostr.tabordalab.com,60.1699,24.9384 +relay.ohstr.com,43.6532,-79.3832 +schnorr.me,43.6532,-79.3832 +relay.s-w.art,43.6532,-79.3832 +relay.nmail.li,50.9871,2.12554 +bcast.girino.org,43.6532,-79.3832 +relay.nostrops.com,32.71,-96.6745 +dm-test-strfry-generic.samt.st,43.6532,-79.3832 +relay.keykeeper.world,40.7824,-74.0711 +relay.satsmarkt.club,52.6907,4.8181 +adre.su,59.9311,30.3609 +relay.edufeed.org:443,49.4521,11.0767 +infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 +relay.lightning.pub,39.0438,-77.4874 +porchlight.social,43.6532,-79.3832 +nostr.snowbla.de,50.4754,12.3683 +relay.trotters.cc:443,43.6532,-79.3832 +nostr-relay.xbytez.io,50.6924,3.20113 +relay.nostr.net,43.6532,-79.3832 +nostrcity-club.fly.dev:443,38.7946,-106.535 +nostr.mikoshi.de,50.1109,8.68213 +nostr.mom,50.4754,12.3683 +schnorr.me:443,43.6532,-79.3832 +chat-relay.zap-work.com:443,43.6532,-79.3832 +relay.paulstephenborile.com:443,49.4543,11.0746 +relay.fckstate.net,59.3293,18.0686 +offchain.bostr.online,43.6532,-79.3832 +nostr.oxtr.dev,50.4754,12.3683 +testnet.samt.st,43.6532,-79.3832 +nostr.planix.org,43.6532,-79.3832 +relay.getvia.xyz,60.1699,24.9384 +relay.lacrypta.ar,43.6532,-79.3832 +nostr.janx.com,43.6532,-79.3832 +relay.yoinekodo.jp,43.6532,-79.3832 +nostrelay.circum.space,52.2245,8.826 +staging.yabu.me,35.6092,139.73 +relay.staging.plebeian.market:443,51.5072,-0.127586 +reraw.pbla2fish.cc,43.6532,-79.3832 +relay.satsapp.me,50.4754,12.3683 +x.kojira.io,43.6532,-79.3832 +relay.layer.systems,49.0291,8.35695 +relay01.lnfi.network,35.6764,139.65 +nostr.whitenode45.ddns.net,40.55,-74.4758 +relay-rpi.edufeed.org,49.4521,11.0767 +nostr-dev.wellorder.net,45.5201,-122.99 +nostr.overmind.lol,43.6532,-79.3832 +relay.aarpia.com,37.3986,-121.964 +relayone.geektank.ai,39.0997,-94.5786 +nostr.purpura.cloud,43.6532,-79.3832 +nostr.data.haus:443,50.4754,12.3683 +nexus.libernet.app:443,43.6532,-79.3832 +nostr.thebiglake.org,32.71,-96.6745 +relay.bowlafterbowl.com,32.9483,-96.7299 +relay.mitchelltribe.com,39.0438,-77.4874 +nostr-relay.xbytez.io:443,50.6924,3.20113 +bitcoiner.social,40.7608,-111.891 +relay.guggero.org,46.5971,9.59652 +relay.mrmave.work,43.6532,-79.3832 +relay.flashapp.me,43.6548,-79.3885 +relay.mmwaves.de,48.8575,2.35138 +nostr.red5d.dev,43.6532,-79.3832 +relay.plebeian.market,50.1109,8.68213 +nostr.girino.org,43.6532,-79.3832 +relay02.lnfi.network,35.6764,139.65 +relay.vrtmrz.net,43.6532,-79.3832 +ynostr.yael.at,60.1699,24.9384 +relay.cosmicbolt.net,37.3986,-121.964 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 nostrelay.circum.space:443,52.2245,8.826 -nostr.notribe.net,40.8302,-74.1299 +nostr.tac.lol:443,47.4748,-122.273 +relay-arg.zombi.cloudrodion.com,1.35208,103.82 +relay.agora.social,50.7383,15.0648 +vault.iris.to:443,43.6532,-79.3832 +relay.nexterz.com,43.6532,-79.3832 +relay2.veganostr.com,60.1699,24.9384 +strfry.shock.network,39.0438,-77.4874 +relay.notoshi.win,13.3396,100.93 +vault.iris.to,43.6532,-79.3832 +relay.novospes.com,43.6532,-79.3832 +nip85.nosfabrica.com,39.0997,-94.5786 +relay-testnet.k8s.layer3.news,37.3387,-121.885 +nostr.stakey.net,52.3676,4.90414 +syb.lol:443,34.0549,-118.243 +nostr-01.uid.ovh,50.9871,2.12554 +nostr-02.yakihonne.com,1.32123,103.695 +relay.favillakey.io,-27.4705,153.026 +relay.plebeian.market:443,50.1109,8.68213 +r.iqbqioza.com,43.6532,-79.3832 +relay.ohstr.com:443,43.6532,-79.3832 +relay.paulstephenborile.com,49.4543,11.0746 +nostr.2b9t.xyz,34.0549,-118.243 +slick.mjex.me,39.0418,-77.4744 +nostr.hekster.org,37.3986,-121.964 +nostr.stakey.net:443,52.3676,4.90414 +relay.sharegap.net,43.6532,-79.3832 +relay.veganostr.com,60.1699,24.9384 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +nostr.data.haus,50.4754,12.3683 +x.kojira.io:443,43.6532,-79.3832 +relay.nostx.io,43.6532,-79.3832 +wheat.happytavern.co,43.6532,-79.3832 +nostr.myshosholoza.co.za:443,52.3676,4.90414 +inbox.scuba323.com,40.8218,-74.45 +nostr.chaima.info,50.1109,8.68213 +relay.nostriches.club,43.6532,-79.3832 +fanfares.nostr1.com,40.7057,-74.0136 +relay.bitmacro.cloud,43.6532,-79.3832 +relay.mostr.pub:443,43.6532,-79.3832 +nostr.spicyz.io:443,43.6532,-79.3832 +relay.agentry.com,42.8864,-78.8784 +nostr.thalheim.io:443,60.1699,24.9384 +tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 +relay.mypathtofire.de,42.8864,-78.8784 +nostr-relay.corb.net:443,39.6478,-104.988 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +nostr.debate.report,50.1109,8.68213 +relay.nostrcheck.me,43.6532,-79.3832 +relay.scuba323.com,40.8218,-74.45 +relay.decentralia.fr,48.122,11.589 +fanfares.nostr1.com:443,40.7057,-74.0136 +relay.sigit.io,50.4754,12.3683 +ribo.nostria.app,43.6532,-79.3832 +nostr.davenov.com,50.1109,8.68213 +nostr.azzamo.net:443,52.2633,21.0283 +ribo.eu.nostria.app,43.6532,-79.3832 +nostr-01.yakihonne.com:443,1.32123,103.695 +desktop-r082364.tail4b3c94.ts.net,48.8566,2.35222 +0x-nostr-relay.fly.dev,38.7946,-106.535 +soloco.nl,43.6532,-79.3832 +relay.chorus.community,48.5333,10.7 +nostr.sathoarder.com:443,48.5734,7.75211 +nostr.myshosholoza.co.za,52.3676,4.90414 +cache.trustr.ing,43.6548,-79.3885 +nostr.islandarea.net,35.4669,-97.6473 +nostr-relay.corb.net,39.6478,-104.988 +relay-rpi.edufeed.org:443,49.4521,11.0767 +rele.speyhard.fi,50.1109,8.68213 +relay.pocketnostr.com,40.8054,-74.0241 +relay.nuts.cash,52.3676,4.90414 +spamspamspamspam.rest,43.6532,-79.3832 +nostr.dlcdevkit.com,40.0992,-83.1141 +relay.satlantis.io,39.0438,-77.4874 +nostr.na.social,43.6532,-79.3832 +relay.nostreon.com,60.1699,24.9384 From 646044cd81ac84a453120c07f0f5b2a02cc7a7e2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:14:05 +0200 Subject: [PATCH 16/45] Clarify capability authentication scope --- docs/ANNOUNCEMENT_GOSSIP.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/ANNOUNCEMENT_GOSSIP.md b/docs/ANNOUNCEMENT_GOSSIP.md index 5bad0711..0328df27 100644 --- a/docs/ANNOUNCEMENT_GOSSIP.md +++ b/docs/ANNOUNCEMENT_GOSSIP.md @@ -34,8 +34,11 @@ Other optional extension: 5 vouch, 6 mesh diagnostics, 7 bridge, 8 authenticated Noise private media, 9 private-media receipts, and 10 reserved non-destructive Noise replacement. Bit 8's exact value is `00 01`. An empty value is valid and means no - advertised capabilities. Unknown bits and bytes beyond the low 64 bits must - be tolerated for forward compatibility. + advertised capabilities. For this announcement TLV, unknown bits and bytes + beyond the low 64 bits must be tolerated for forward compatibility. This + tolerance does not apply to the Noise-authenticated peer-state payload + `0x21`, whose capability field must be canonical and 1...8 bytes; see + `PRIVATE_MEDIA_V1.md`. New TLV (optional): @@ -76,6 +79,9 @@ This matches the on‑wire 8‑byte `senderID`/`recipientID` encoding used in th - Capability bits are independent discovery hints. Implementations advertise only wire features they implement and must not infer support from platform, app version, or unrelated transports with similar names. +- Private-media receipts and retry (bit 9) require the current Noise + generation's authenticated `0x21` peer state to prove both bits 8 and 9. An + announcement alone never authorizes that behavior. - If a `0x04` TLV is present: - Interpret the value as `N = length / 8` peer IDs (ignore trailing non‑aligned bytes). - Each 8‑byte chunk is decoded back to a 16‑hex‑char peer ID string (lowercase). From 03d1da5423b2ee808cee833c32862a3175a1bced Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:13:19 +0200 Subject: [PATCH 17/45] Fix CommandProcessor test syntax --- .../test/java/com/bitchat/android/ui/CommandProcessorTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt index 985e9a9f..2e52b750 100644 --- a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt @@ -180,6 +180,7 @@ class CommandProcessorTest { assertEquals("#backchannel", chatState.getCurrentChannelValue()) assertEquals(ChannelID.Mesh, chatState.selectedLocationChannel.value) + } @Test fun `clearSuggestions hides the command suggestion popup`() { @@ -207,4 +208,4 @@ class CommandProcessorTest { assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) } -} \ No newline at end of file +} From d55754d82451f6058388e74bead34234e001a4b5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 23 Aug 2026 06:09:25 +0000 Subject: [PATCH 18/45] Automated update of relay data - Sun Aug 23 06:09:25 UTC 2026 --- app/src/main/assets/nostr_relays.csv | 800 +++++++++++++-------------- 1 file changed, 392 insertions(+), 408 deletions(-) diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index 0d46c11e..a503b348 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,416 +1,400 @@ Relay URL,Latitude,Longitude -nostr.bitcoiner.social,40.7608,-111.891 -relay.mostr.pub,43.6532,-79.3832 -relay.veganostr.com:443,60.1699,24.9384 -relay.dreamith.to,43.6532,-79.3832 -buzz.cashu.space,50.1109,8.68213 -relay.sincensura.org,43.6532,-79.3832 -relayrs.notoshi.win:443,43.6532,-79.3832 -nostr.bond,50.1109,8.68213 -relay.edufeed.org,49.4521,11.0767 -relay.bullishbounty.com:443,43.6532,-79.3832 -relay.nostr.blockhenge.com,39.0438,-77.4874 -relay2.fiatdenier.com,50.1013,8.62643 -relay.mwaters.net,50.9871,2.12554 -relay2.angor.io,48.1046,11.6002 -memlay.v0l.io,53.3498,-6.26031 -nostr.wecsats.io,43.6532,-79.3832 -vm-1734.lnvps.cloud,53.3498,-6.26031 -dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 -nostr2.girino.org:443,43.6532,-79.3832 -nostr.robosats.org,64.1476,-21.9392 -relay.atsocy.com,43.6532,-79.3832 -relayone.geektank.ai:443,39.0997,-94.5786 -relay.olas.app:443,60.1699,24.9384 -relay.nostu.be,40.4167,-3.70329 -relay.otrta.me,50.1109,8.68213 -nostr.spaceshell.xyz,43.6532,-79.3832 -rilo.nostria.app,43.6532,-79.3832 -relay.agorist.space,52.3734,4.89406 -relay.nostrmap.net,60.1699,24.9384 -relay.bullishbounty.com,43.6532,-79.3832 -nrs-01.darkcloudarcade.com,39.0997,-94.5786 -relay.angor.io:443,48.1046,11.6002 -nostr.rtvslawenia.com,49.4543,11.0746 -relay.wasabiwallet.io,43.6532,-79.3832 -cs-relay.nostrdev.com:443,50.4754,12.3683 -nostr.4rs.nl,49.0291,8.35696 -nexus.libernet.app,43.6532,-79.3832 -relay-dev.gulugulu.moe,43.6532,-79.3832 -nostr.88mph.life,52.1941,-2.21905 -relayone.soundhsa.com:443,39.0997,-94.5786 -social.amanah.eblessing.co,48.1046,11.6002 -relay.getsafebox.app,43.6532,-79.3832 -relay.44billion.net,43.6532,-79.3832 -relay.conduit.market,38.7946,-106.535 -nostr.infero.net,35.6764,139.65 -relay.staging.plebeian.market,51.5072,-0.127586 -relay.snort.social,53.3498,-6.26031 -purplerelay.com:443,43.6532,-79.3832 -nostr.mas-family.eu,60.3478,15.7505 -relay.ru.ac.th,13.7607,100.627 -relay.openresist.com,43.6532,-79.3832 -antiprimal.net,43.6532,-79.3832 -relay.illuminodes.com,43.6532,-79.3832 -relay.mmwaves.de:443,48.8575,2.35138 -nostr.spicyz.io,43.6532,-79.3832 -relay.nostrfeed.com,60.1699,24.9384 -strfry.apps3.slidestr.net,40.4167,-3.70329 -relay.directsponsor.net,42.8864,-78.8784 -relay.nostrhub.fr,48.1045,11.6004 -nostr.plantroon.com,50.1013,8.62643 -nostr-verified.wellorder.net,45.5201,-122.99 -relay.bnos.space:443,43.6532,-79.3832 -relay.pyramid.li,47.4093,8.46503 -public.crostr.com,43.6532,-79.3832 -relay.kilombino.com,43.6532,-79.3832 -relay.libernet.app:443,43.6532,-79.3832 -relay.internationalright-wing.org,-22.4692,-48.9875 -nostr.overmind.lol:443,43.6532,-79.3832 -relay.ordoplay.com,50.1109,8.68213 -mostro-p2p.tech,50.1109,8.68213 -offchain.pub:443,39.1585,-94.5728 -cs-relay.nostrdev.com,50.4754,12.3683 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 -nostr.unkn0wn.world,46.8499,9.53287 -relay.shadowbip.com,50.1109,8.68213 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -relay.staging.commonshub.brussels,49.4543,11.0746 -relay2.angor.io:443,48.1046,11.6002 -relay.arx-ccn.com,50.4754,12.3683 -relay.underorion.se,50.1109,8.68213 -bread.nostrsms.com,40.8218,-74.45 -relay.cypherflow.ai,48.8575,2.35138 -nostr.islandarea.net:443,35.4669,-97.6473 -relay.homeinhk.xyz,35.694,139.754 -relay.ditto.pub:443,43.6532,-79.3832 -nostr.fullstackcash.net,45.5201,-122.99 -bitcoinostr.duckdns.org,38.9504,-0.14007 -relay.trotters.cc,43.6532,-79.3832 -portal-relay.pareto.space,49.0291,8.35696 -relay.openresist.com:443,43.6532,-79.3832 -relay.endfiat.money,59.3327,18.0656 -nostr.hoppe-relay.it.com,42.8864,-78.8784 -nostr.sovereignservices.xyz,43.6532,-79.3832 -relay.fundstr.me,42.3601,-71.0589 -relay5.bitransfer.org,43.6532,-79.3832 -nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 -testr.nymble.world,40.8054,-74.0241 -strfry.shock.network:443,39.0438,-77.4874 -chorus.mikedilger.com:444,-36.8906,174.794 -relay.minibolt.info,43.6532,-79.3832 -nos.lol:443,50.4754,12.3683 -relay.liberbitworld.org,43.6532,-79.3832 -maxq.descendant.io,43.6532,-79.3832 -nostrcity-club.fly.dev,38.7946,-106.535 -relay.nostriot.com,41.5695,-83.9786 -nostr.n7ekb.net,47.4941,-122.294 -relay.erybody.com,41.4513,-81.7021 -nostr-rs-relay-qj1h.onrender.com,37.7775,-122.397 -nostrrelay.taylorperron.com,45.5029,-73.5723 -syb.lol,34.0549,-118.243 -relay.nostr.com,50.1109,8.68213 -relay.hivetalk.org,40.8302,-74.1299 -relay.degmods.com,50.4754,12.3683 -relay.laantungir.net,-19.4692,-42.5315 -relay.nearhood.co.uk,51.5134,-0.0890675 -relay.tdw.lol,41.8781,-87.6298 -ribo.us.nostria.app,43.6532,-79.3832 +myvoiceourstory.org,37.3598,-121.981 +nostr.linky.fit,50.1109,8.68213 public.obelisk.ar,43.6532,-79.3832 -prl.plus,55.7628,37.5983 -21milionidinostr.duckdns.org,45.5192,9.08625 -nostr.carroarmato0.be,50.914,3.21378 -nostr.pbfs.io:443,50.4754,12.3683 -public.crostr.com:443,43.6532,-79.3832 -espelho.girino.org,43.6532,-79.3832 -nostr.computingcache.com,45.5341,-122.956 -relay.chorus.community:443,48.5333,10.7 -nostr-relay.nextblockvending.com,47.2343,-119.853 -nostr.easycryptosend.it,43.6532,-79.3832 -no.str.cr:443,8.96171,-83.5246 -nostr.snowbla.de:443,50.4754,12.3683 -bridge.tagomago.me,42.3601,-71.0589 -relay.endfiat.money:443,59.3327,18.0656 -relay.klabo.world,47.674,-122.122 -articles.layer3.news:443,37.3387,-121.885 -nostr.21crypto.ch,47.5356,8.73209 -relay.kaleidoswap.com,50.8476,4.35717 -nostr.relay.hedwig.sh,60.1699,24.9384 -nostr.yutakobayashi.com,43.6532,-79.3832 -strfry.ymir.cloud,43.6532,-79.3832 -relay.littlebitstudios.com,43.6532,-79.3832 -testnet-relay.samt.st:443,40.8302,-74.1299 -relay.islandbitcoin.com,12.8498,77.6545 -relay.zone667.com,60.1699,24.9384 -relay.lightning.pub:443,39.0438,-77.4874 -relay.layer.systems:443,49.0291,8.35695 -offchain.pub,39.1585,-94.5728 -nos.lol,50.4754,12.3683 -cdn.satellite.earth,40.8302,-74.1299 +relay.i9.eti.br,43.6532,-79.3832 +yabu.me,35.6092,139.73 relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -relay.wisp.talk:443,49.4543,11.0746 -nostr.hekster.org:443,37.3986,-121.964 -nostr.mom:443,50.4754,12.3683 -relay.mccormick.cx:443,52.3563,4.95714 -dev.relay.stream,43.6532,-79.3832 -dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -relay.vrtmrz.net:443,43.6532,-79.3832 -relayone.soundhsa.com,39.0997,-94.5786 -relay.agorist.space:443,52.3734,4.89406 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -nostr.dlcdevkit.com:443,40.0992,-83.1141 -node.kommonzenze.de,49.4521,11.0767 -relay.beamhop.com,43.6532,-79.3832 -nostr.2b9t.xyz:443,34.0549,-118.243 -relay.chatbett.de,40.7128,-74.006 -relay.tdw.gg,34.0549,-118.243 +relay.nostu.be,40.4167,-3.70329 +relay.favillakey.io,-27.4705,153.026 +nostr.mikoshi.de,50.1109,8.68213 +relay.agilesolutionlabs.com,43.6532,-79.3832 +nostr.sovereignservices.xyz,43.6532,-79.3832 +relay.qstr.app,50.1109,8.68213 +articles.layer3.news:443,37.3387,-121.885 +relay.nostrops.com,32.71,-96.6745 +relay.mrmave.work,43.6532,-79.3832 +nostr.islandarea.net:443,35.4669,-97.6473 +nostr.azzamo.net:443,52.2633,21.0283 +ec2.f7z.io,60.1699,24.9384 +relay.veganostr.com,60.1699,24.9384 +nostr.dlcdevkit.com,40.0992,-83.1141 +relay.44billion.net,43.6532,-79.3832 +bendernostur.duckdns.org:8443,50.1109,8.68213 +nostras.app,38.7946,-106.535 +cs-relay.nostrdev.com:443,50.4754,12.3683 +nostr.stakey.net,52.3676,4.90414 +support.flotilla.social,32.9483,-96.7299 +relay.ordoplay.com,50.1109,8.68213 +nostr.stakey.net:443,52.3676,4.90414 +relay.nostrmap.net,60.1699,24.9384 +relayone.geektank.ai,39.0997,-94.5786 +relay.novospes.com,43.6532,-79.3832 +relay.dyne.org,49.0291,8.35705 +nostrrelay.taylorperron.com,45.5029,-73.5723 +nostrbtc.com,43.6532,-79.3832 +relay.lanavault.space,60.1699,24.9384 +relay.bullishbounty.com:443,43.6532,-79.3832 +relay.layer.systems:443,49.0291,8.35695 +vault.iris.to:443,43.6532,-79.3832 +relay.getsafebox.app:443,43.6532,-79.3832 +relay.wellorder.net,45.5201,-122.99 +relay.scuba323.com,40.8218,-74.45 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +temp.iris.to,43.6532,-79.3832 +relay.paulstephenborile.com,49.4543,11.0746 +relay.cyberguy.fyi,52.6907,4.8181 +nostr.wecsats.io:443,43.6532,-79.3832 +bread.nostrsms.com,40.8218,-74.45 +relay.plebeian.market,50.1109,8.68213 +relay.laantungir.net,-19.4692,-42.5315 +nostr.overmind.lol,43.6532,-79.3832 +nip85.nosfabrica.com,39.0997,-94.5786 +relay.conduit.market,38.7946,-106.535 +relay.sigit.io,50.4754,12.3683 +relay.dreamith.to,43.6532,-79.3832 +relay.decentralia.fr,48.122,11.589 +nexus.libernet.app,43.6532,-79.3832 +relay.nmail.li,50.9871,2.12554 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +nostr-relay.nextblockvending.com,47.2343,-119.853 +nostr-relay.corb.net,39.6478,-104.988 +relay.agentry.com,42.8864,-78.8784 budabit.nostr1.com,40.7057,-74.0136 -nostr.novacisko.cz,52.2026,20.9397 +relay.cypherflow.ai,48.8575,2.35138 +purplerelay.com,43.6532,-79.3832 +nostr.islandarea.net,35.4669,-97.6473 +relay.edufeed.org,49.4521,11.0767 +relay.bnos.space,43.6532,-79.3832 +relay.kizuna-miki.com,35.694,139.754 +nostr.snowbla.de,50.4754,12.3683 +bitchat.nostr1.com,40.7057,-74.0136 +relay.beamhop.com,43.6532,-79.3832 +relay.wavefunc.live,41.8781,-87.6298 +relayrs.notoshi.win,43.6532,-79.3832 +relay.nostrhub.fr,48.1045,11.6004 +relay01.lnfi.network,35.6764,139.65 +relay.bitos.space,43.6532,-79.3832 +relay.noeudlibre.fr,50.6924,3.20113 +relay.orly.dev,32.7767,-96.797 +relay.paulstephenborile.com:443,49.4543,11.0746 +relay.atsocy.com,43.6532,-79.3832 +relay.keykeeper.world,40.7824,-74.0711 +nostrmxn.lulus.com.mx,59.4016,17.9455 +maxq.descendant.io,43.6532,-79.3832 +nostr.spicyz.io:443,43.6532,-79.3832 +nostr.chaima.info:443,50.1109,8.68213 +relay.opmaat.org,60.1699,24.9384 +nostr.notribe.net,40.8302,-74.1299 +nostr-01.yakihonne.com:443,1.32123,103.695 +offchain.pub,39.1585,-94.5728 +relay.agora.social,50.7383,15.0648 +relay.tdw.lol,41.8781,-87.6298 +nostr.nodesmap.com,59.3327,18.0656 +relay.getvia.xyz,60.1699,24.9384 +public.crostr.com,43.6532,-79.3832 +relay.agorist.space,52.3734,4.89406 +relay.lacrypta.ar,43.6532,-79.3832 +nostr.spaceshell.xyz,43.6532,-79.3832 +relay.nuts.cash,52.3676,4.90414 +relay.otrta.me,50.1109,8.68213 +relay.satmaxt.xyz,43.6532,-79.3832 +relay.earthly.city,34.1749,-118.54 +relay.wisp.talk:443,49.4543,11.0746 +mostro-p2p.tech,50.1109,8.68213 +nostr.thalheim.io,60.1699,24.9384 +relay.s-w.art,43.6532,-79.3832 +nostr.chaima.info,50.1109,8.68213 +articles.layer3.news,37.3387,-121.885 +nostr2.girino.org:443,43.6532,-79.3832 +relay.mostr.pub:443,43.6532,-79.3832 +relay.ditto.pub:443,43.6532,-79.3832 +relay.manneken.brussels,49.4543,11.0746 +relay.kaleidoswap.com,50.8476,4.35717 +nostrelay.circum.space,52.2245,8.826 +relay.beginningend.com,35.2227,-97.4786 +relay-dev.gulugulu.moe,43.6532,-79.3832 +relay.fundstr.me,42.3601,-71.0589 +relay.jmoose.rocks:443,37.4419,-122.143 +relay.nostr.dev.br,48.1046,11.6002 +relay.minibolt.info,43.6532,-79.3832 +nostr.unkn0wn.world,46.8499,9.53287 +vault.iris.to,43.6532,-79.3832 +relay.guggero.org,46.5971,9.59652 +relay.getsafebox.app,43.6532,-79.3832 +relay.bitmacro.cloud,43.6532,-79.3832 +relay.wavlake.com,41.2619,-95.8608 +nostr.hekster.org:443,37.3986,-121.964 +relay.lightning.pub,39.0438,-77.4874 +relay-dev.gulugulu.moe:443,43.6532,-79.3832 +soloco.nl,43.6532,-79.3832 +nostr.hifish.org,47.4009,8.57814 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +dm-test-strfry-generic.samt.st,43.6532,-79.3832 +relay.wavefunc.live:443,41.8781,-87.6298 +porchlight.social,43.6532,-79.3832 +relay.zone667.com,60.1699,24.9384 +nostr.data.haus:443,50.4754,12.3683 +relay.nexterz.com,43.6532,-79.3832 +relay.sigit.io:443,50.4754,12.3683 +nostr.girino.org,43.6532,-79.3832 +nostr.liberty.fans,36.8767,-89.5879 +wheat.happytavern.co,43.6532,-79.3832 +relay.0xchat.com,43.6532,-79.3832 +inbox.scuba323.com,40.8218,-74.45 +nostr.vulpem.com,49.4543,11.0746 +relay.damustr.com,43.6532,-79.3832 +no.str.cr:443,8.96171,-83.5246 +relay.vrtmrz.net:443,43.6532,-79.3832 +vm-1734.lnvps.cloud,53.3498,-6.26031 +testnet.samt.st,43.6532,-79.3832 +nostr.rtvslawenia.com:443,49.4543,11.0746 +nostr.sathoarder.com:443,48.5734,7.75211 +relay.cosmicbolt.net,37.3986,-121.964 +relay.staging.plebeian.market,51.5072,-0.127586 +relayone.soundhsa.com,39.0997,-94.5786 +relay.olas.app:443,60.1699,24.9384 +relay.illuminodes.com,43.6532,-79.3832 +relay.chorus.community:443,48.5333,10.7 +relay.ohstr.com:443,43.6532,-79.3832 +fanfares.nostr1.com:443,40.7057,-74.0136 +nostr.planix.org,43.6532,-79.3832 +schnorr.me,43.6532,-79.3832 +relay.mostro.network,40.8302,-74.1299 +nostr-verified.wellorder.net,45.5201,-122.99 +ynostr.yael.at,60.1699,24.9384 +nostr.wild-vibes.ts.net,48.8566,2.35222 +relay.mypathtofire.de,42.8864,-78.8784 +relay.klabo.world,47.674,-122.122 +nostr.tagomago.me,42.3601,-71.0589 +relay.veganostr.com:443,60.1699,24.9384 +nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 +relay.loveisbitcoin.com,43.6532,-79.3832 +bridge.tagomago.me,42.3601,-71.0589 +relay-rpi.edufeed.org:443,49.4521,11.0767 +relay.kilombino.com,43.6532,-79.3832 +relay.sharegap.net,43.6532,-79.3832 +nostr.yutakobayashi.com,43.6532,-79.3832 +relay-na1.metanomalist.com,43.6532,-79.3832 +public.crostr.com:443,43.6532,-79.3832 +nostrride.io,37.3986,-121.964 +nostr.bgbitcoin.club:443,50.4754,12.3683 +nostr.christiansass.de,50.1109,8.68213 +nos.lol:443,50.4754,12.3683 +relay.pyramid.li,47.4093,8.46503 +relay.mappingbitcoin.com,43.6532,-79.3832 +nostr-relay.zimage.com,34.282,-118.439 +offchain.pub:443,39.1585,-94.5728 +nostr.myshosholoza.co.za:443,52.3676,4.90414 +0x-nostr-relay.fly.dev,38.7946,-106.535 +relay1.privkey.io,25.7617,-80.1918 +chat-relay.zap-work.com:443,43.6532,-79.3832 +relay.staging.plebeian.market:443,51.5072,-0.127586 +nostr.robosats.org,64.1476,-21.9392 +relay.endfiat.money:443,59.3327,18.0656 +nostr.agh.ccwu.cc,43.6532,-79.3832 +nostr.overmind.lol:443,43.6532,-79.3832 +nostr.tac.lol,47.4748,-122.273 +relay.ru.ac.th,13.7607,100.627 +nostr-relay.xbytez.io,50.6924,3.20113 +relay.islandbitcoin.com,12.8498,77.6545 +relay5.bitransfer.org,43.6532,-79.3832 +prl.plus,55.7628,37.5983 +chat-relay.zap-work.com,43.6532,-79.3832 +relay.satlantis.io,39.0438,-77.4874 +nostr.bitcoiner.social:443,40.7608,-111.891 +nostr.ac,38.958,-77.3592 +relay.solomonstr.com,43.6532,-79.3832 +strfry.apps3.slidestr.net,40.4167,-3.70329 +relay.nostr.com,50.1109,8.68213 +reraw.pbla2fish.cc,43.6532,-79.3832 +nostr.bond,50.1109,8.68213 +relay.wavlake.com:443,41.2619,-95.8608 +nos.lol,50.4754,12.3683 +nostr.debate.report,50.1109,8.68213 +relay-testnet.k8s.layer3.news,37.3387,-121.885 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +relay.wisp.talk,49.4543,11.0746 +nostr.tabordalab.com,60.1699,24.9384 +relay.trotters.cc,43.6532,-79.3832 +nostr.4rs.nl,49.0291,8.35696 +relay.hivetalk.org,40.8302,-74.1299 +spamspamspamspam.rest,43.6532,-79.3832 +nostr.janx.com,43.6532,-79.3832 +relay.mostr.pub,43.6532,-79.3832 +relay.lightning.pub:443,39.0438,-77.4874 relay.samt.st,40.8302,-74.1299 bruh.samt.st,43.6532,-79.3832 -nostr.wild-vibes.ts.net,48.8566,2.35222 -relay.opossumwire.org,34.0549,-118.243 -relay.angor.io,48.1046,11.6002 -relay.nostrmap.net:443,60.1699,24.9384 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -chat-relay.zap-work.com,43.6532,-79.3832 -relay.earthly.city,34.1749,-118.54 -relay.mappingbitcoin.com,43.6532,-79.3832 -relay.satmaxt.xyz,43.6532,-79.3832 -relay.primal.net,43.6532,-79.3832 -relay.bornheimer.app,50.1109,8.68213 -nostr.tagomago.me,42.3601,-71.0589 -relay.orly.dev,32.7767,-96.797 -no.str.cr,8.96171,-83.5246 -auth.nostr1.com,40.7057,-74.0136 -relay.opmaat.org,60.1699,24.9384 -temp.iris.to,43.6532,-79.3832 -conduitl2.fly.dev,38.7946,-106.535 -yabu.me,35.6092,139.73 -nostrride.io,37.3986,-121.964 -nostr.plantroon.com:443,50.1013,8.62643 -relay.wavlake.com,41.2619,-95.8608 -nostrbtc.com,43.6532,-79.3832 -relay.mitchelltribe.com:443,39.0438,-77.4874 -relay.manneken.brussels,49.4543,11.0746 -relay.lanacoin-eternity.com,40.8302,-74.1299 -bendernostur.duckdns.org:8443,50.1109,8.68213 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -nostr.nodesmap.com,59.3327,18.0656 -nostr-01.yakihonne.com,1.32123,103.695 -bunk-test.feeds.relay.tools,38.6327,-90.1961 -relay.btcforplebs.com,43.6532,-79.3832 -relay.beginningend.com,35.2227,-97.4786 -nostr.iskarion.ddns.net,43.3076,-2.95421 -nostr-relay.cbrx.io,43.6532,-79.3832 -relay.nostrian-conquest.com,41.223,-111.974 -relay.cosmicbolt.net:443,37.3986,-121.964 -nostr.oxtr.dev:443,50.4754,12.3683 -nostr-relay.acloud.kdns.fr,43.6532,-79.3832 -relay.mccormick.cx,52.3563,4.95714 -relay.fiatdenier.com,43.7787,-79.5393 -nostr.bitcoiner.social:443,40.7608,-111.891 -relay.gulugulu.moe:443,43.6532,-79.3832 -relay.piazza.today,48.122,11.589 -nostr.ac,38.958,-77.3592 -strfry.bonsai.com,39.0438,-77.4874 -dev-relay.nostreon.com,60.1699,24.9384 -relay.olas.app,60.1699,24.9384 -relay.loveisbitcoin.com,43.6532,-79.3832 -strfry.bonsai.com:443,39.0438,-77.4874 -nostr.tac.lol,47.4748,-122.273 -rilo.nostria.app:443,43.6532,-79.3832 -nostr.azzamo.net,52.2633,21.0283 -nostr.wecsats.io:443,43.6532,-79.3832 -relay.wellorder.net,45.5201,-122.99 -relay.libernet.app,43.6532,-79.3832 -nostr.thalheim.io,60.1699,24.9384 -relayrs.notoshi.win,43.6532,-79.3832 -ribo.nostria.app:443,43.6532,-79.3832 -relay.gulugulu.moe,43.6532,-79.3832 -relay.qstr.app,50.1109,8.68213 -myvoiceourstory.org,37.3598,-121.981 -nostr-relay.zimage.com,34.282,-118.439 -treuzkas.branruz.com,48.8575,2.35138 -top.testrelay.top,43.6532,-79.3832 -relay.cyberguy.fyi,52.6907,4.8181 -articles.layer3.news,37.3387,-121.885 -nostr.sathoarder.com,48.5734,7.75211 -nostr.carroarmato0.be:443,50.914,3.21378 -nostr-02.uid.ovh,50.9871,2.12554 -ribo.us.nostria.app:443,43.6532,-79.3832 -relay.ditto.pub,43.6532,-79.3832 -nostr.chaima.info:443,50.1109,8.68213 -relay.bnos.space,43.6532,-79.3832 -relay.solife.me,43.6532,-79.3832 -relay.wavlake.com:443,41.2619,-95.8608 -bucket.coracle.social,37.7775,-122.397 -nostr.christiansass.de,52.5244,13.4105 -relay.dyne.org,49.0291,8.35705 -testnet-relay.samt.st,40.8302,-74.1299 -nostream.seitendan.com,34.7062,135.493 -basspistol.org,49.0291,8.35696 -purplerelay.com,43.6532,-79.3832 -relay.wisp.talk,49.4543,11.0746 -relay.getsafebox.app:443,43.6532,-79.3832 -freelay.sovbit.host,60.1699,24.9384 -relay.sigit.io:443,50.4754,12.3683 -buzz.ac2n-share.kozow.com,45.764,4.83566 -relay.mostro.network,40.8302,-74.1299 -nostrcheck.me,43.6532,-79.3832 -nostr.rtvslawenia.com:443,49.4543,11.0746 -insta-relay.apps3.slidestr.net,40.4167,-3.70329 -nostr.vulpem.com,49.4543,11.0746 -relay-na1.metanomalist.com,43.6532,-79.3832 -chorus.bonsai.com,39.0438,-77.4874 -ec2.f7z.io,60.1699,24.9384 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -nostr.twinkle.lol,51.902,7.6657 -relay.lanavault.space,60.1699,24.9384 -nostr.pbfs.io,50.4754,12.3683 -bitchat.nostr1.com,40.7057,-74.0136 -nostr.highway15.net,28.0445,-82.6699 -nostr.tabordalab.com,60.1699,24.9384 -relay.ohstr.com,43.6532,-79.3832 -schnorr.me,43.6532,-79.3832 -relay.s-w.art,43.6532,-79.3832 -relay.nmail.li,50.9871,2.12554 -bcast.girino.org,43.6532,-79.3832 -relay.nostrops.com,32.71,-96.6745 -dm-test-strfry-generic.samt.st,43.6532,-79.3832 -relay.keykeeper.world,40.7824,-74.0711 -relay.satsmarkt.club,52.6907,4.8181 -adre.su,59.9311,30.3609 -relay.edufeed.org:443,49.4521,11.0767 -infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 -relay.lightning.pub,39.0438,-77.4874 -porchlight.social,43.6532,-79.3832 -nostr.snowbla.de,50.4754,12.3683 -relay.trotters.cc:443,43.6532,-79.3832 -nostr-relay.xbytez.io,50.6924,3.20113 -relay.nostr.net,43.6532,-79.3832 -nostrcity-club.fly.dev:443,38.7946,-106.535 -nostr.mikoshi.de,50.1109,8.68213 -nostr.mom,50.4754,12.3683 -schnorr.me:443,43.6532,-79.3832 -chat-relay.zap-work.com:443,43.6532,-79.3832 -relay.paulstephenborile.com:443,49.4543,11.0746 -relay.fckstate.net,59.3293,18.0686 -offchain.bostr.online,43.6532,-79.3832 -nostr.oxtr.dev,50.4754,12.3683 -testnet.samt.st,43.6532,-79.3832 -nostr.planix.org,43.6532,-79.3832 -relay.getvia.xyz,60.1699,24.9384 -relay.lacrypta.ar,43.6532,-79.3832 -nostr.janx.com,43.6532,-79.3832 -relay.yoinekodo.jp,43.6532,-79.3832 -nostrelay.circum.space,52.2245,8.826 -staging.yabu.me,35.6092,139.73 -relay.staging.plebeian.market:443,51.5072,-0.127586 -reraw.pbla2fish.cc,43.6532,-79.3832 -relay.satsapp.me,50.4754,12.3683 -x.kojira.io,43.6532,-79.3832 -relay.layer.systems,49.0291,8.35695 -relay01.lnfi.network,35.6764,139.65 -nostr.whitenode45.ddns.net,40.55,-74.4758 -relay-rpi.edufeed.org,49.4521,11.0767 -nostr-dev.wellorder.net,45.5201,-122.99 -nostr.overmind.lol,43.6532,-79.3832 -relay.aarpia.com,37.3986,-121.964 -relayone.geektank.ai,39.0997,-94.5786 -nostr.purpura.cloud,43.6532,-79.3832 -nostr.data.haus:443,50.4754,12.3683 -nexus.libernet.app:443,43.6532,-79.3832 -nostr.thebiglake.org,32.71,-96.6745 -relay.bowlafterbowl.com,32.9483,-96.7299 -relay.mitchelltribe.com,39.0438,-77.4874 -nostr-relay.xbytez.io:443,50.6924,3.20113 -bitcoiner.social,40.7608,-111.891 -relay.guggero.org,46.5971,9.59652 -relay.mrmave.work,43.6532,-79.3832 -relay.flashapp.me,43.6548,-79.3885 -relay.mmwaves.de,48.8575,2.35138 -nostr.red5d.dev,43.6532,-79.3832 -relay.plebeian.market,50.1109,8.68213 -nostr.girino.org,43.6532,-79.3832 -relay02.lnfi.network,35.6764,139.65 -relay.vrtmrz.net,43.6532,-79.3832 -ynostr.yael.at,60.1699,24.9384 -relay.cosmicbolt.net,37.3986,-121.964 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -nostrelay.circum.space:443,52.2245,8.826 -nostr.tac.lol:443,47.4748,-122.273 -relay-arg.zombi.cloudrodion.com,1.35208,103.82 -relay.agora.social,50.7383,15.0648 -vault.iris.to:443,43.6532,-79.3832 -relay.nexterz.com,43.6532,-79.3832 -relay2.veganostr.com,60.1699,24.9384 -strfry.shock.network,39.0438,-77.4874 +relay.orangesync.tech,40.7128,-74.006 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 relay.notoshi.win,13.3396,100.93 -vault.iris.to,43.6532,-79.3832 -relay.novospes.com,43.6532,-79.3832 -nip85.nosfabrica.com,39.0997,-94.5786 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -nostr.stakey.net,52.3676,4.90414 -syb.lol:443,34.0549,-118.243 -nostr-01.uid.ovh,50.9871,2.12554 -nostr-02.yakihonne.com,1.32123,103.695 -relay.favillakey.io,-27.4705,153.026 -relay.plebeian.market:443,50.1109,8.68213 -r.iqbqioza.com,43.6532,-79.3832 -relay.ohstr.com:443,43.6532,-79.3832 -relay.paulstephenborile.com,49.4543,11.0746 -nostr.2b9t.xyz,34.0549,-118.243 -slick.mjex.me,39.0418,-77.4744 -nostr.hekster.org,37.3986,-121.964 -nostr.stakey.net:443,52.3676,4.90414 -relay.sharegap.net,43.6532,-79.3832 -relay.veganostr.com,60.1699,24.9384 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -nostr.data.haus,50.4754,12.3683 -x.kojira.io:443,43.6532,-79.3832 -relay.nostx.io,43.6532,-79.3832 -wheat.happytavern.co,43.6532,-79.3832 -nostr.myshosholoza.co.za:443,52.3676,4.90414 -inbox.scuba323.com,40.8218,-74.45 -nostr.chaima.info,50.1109,8.68213 -relay.nostriches.club,43.6532,-79.3832 -fanfares.nostr1.com,40.7057,-74.0136 -relay.bitmacro.cloud,43.6532,-79.3832 -relay.mostr.pub:443,43.6532,-79.3832 -nostr.spicyz.io:443,43.6532,-79.3832 -relay.agentry.com,42.8864,-78.8784 -nostr.thalheim.io:443,60.1699,24.9384 -tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 -relay.mypathtofire.de,42.8864,-78.8784 -nostr-relay.corb.net:443,39.6478,-104.988 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -nostr.debate.report,50.1109,8.68213 -relay.nostrcheck.me,43.6532,-79.3832 -relay.scuba323.com,40.8218,-74.45 -relay.decentralia.fr,48.122,11.589 -fanfares.nostr1.com:443,40.7057,-74.0136 -relay.sigit.io,50.4754,12.3683 -ribo.nostria.app,43.6532,-79.3832 -nostr.davenov.com,50.1109,8.68213 -nostr.azzamo.net:443,52.2633,21.0283 -ribo.eu.nostria.app,43.6532,-79.3832 -nostr-01.yakihonne.com:443,1.32123,103.695 -desktop-r082364.tail4b3c94.ts.net,48.8566,2.35222 -0x-nostr-relay.fly.dev,38.7946,-106.535 -soloco.nl,43.6532,-79.3832 -relay.chorus.community,48.5333,10.7 -nostr.sathoarder.com:443,48.5734,7.75211 -nostr.myshosholoza.co.za,52.3676,4.90414 -cache.trustr.ing,43.6548,-79.3885 -nostr.islandarea.net,35.4669,-97.6473 -nostr-relay.corb.net,39.6478,-104.988 -relay-rpi.edufeed.org:443,49.4521,11.0767 -rele.speyhard.fi,50.1109,8.68213 -relay.pocketnostr.com,40.8054,-74.0241 -relay.nuts.cash,52.3676,4.90414 -spamspamspamspam.rest,43.6532,-79.3832 -nostr.dlcdevkit.com,40.0992,-83.1141 -relay.satlantis.io,39.0438,-77.4874 -nostr.na.social,43.6532,-79.3832 relay.nostreon.com,60.1699,24.9384 +nostr.purpura.cloud,43.6532,-79.3832 +basspistol.org,49.0291,8.35696 +relay.nostrfeed.com,60.1699,24.9384 +nostr.dlcdevkit.com:443,40.0992,-83.1141 +nostr.21crypto.ch,47.5356,8.73209 +nostr.pbfs.io,50.4754,12.3683 +social.amanah.eblessing.co,48.1046,11.6002 +relay.cosmicbolt.net:443,37.3986,-121.964 +21milionidinostr.duckdns.org,41.8967,12.4822 +fanfares.nostr1.com,40.7057,-74.0136 +nostr.na.social,43.6532,-79.3832 +nostr.data.haus,50.4754,12.3683 +strfry.shock.network:443,39.0438,-77.4874 +nostr.pbfs.io:443,50.4754,12.3683 +relay.shadowbip.com,50.1109,8.68213 +relay.ltgnet.work:8443,51.0511,-114.075 +relay02.lnfi.network,35.6764,139.65 +nostr.myshosholoza.co.za,52.3676,4.90414 +relay.satsmarkt.club,52.6907,4.8181 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 +auth.nostr1.com,40.7057,-74.0136 +dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 +bitcoinostr.duckdns.org,38.9504,-0.14007 +relayrs.notoshi.win:443,43.6532,-79.3832 +nostr.whitenode45.ddns.net,40.55,-74.4758 +relay.mccormick.cx:443,52.3563,4.95714 +nostr.2b9t.xyz:443,34.0549,-118.243 +relay.internationalright-wing.org,-22.4692,-48.9875 +nostr.2b9t.xyz,34.0549,-118.243 +relay.mitchelltribe.com,39.0438,-77.4874 +relay.nostriot.com,41.5695,-83.9786 +infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 +relay.sector01.com,41.4513,-81.7021 +relay.nostx.io,43.6532,-79.3832 +relay.fiatdenier.com,43.7787,-79.5393 +relayone.soundhsa.com:443,39.0997,-94.5786 +strfry.bonsai.com:443,39.0438,-77.4874 +nostr.snowbla.de:443,50.4754,12.3683 +relay.btcforplebs.com,43.6532,-79.3832 +dev.relay.stream,43.6532,-79.3832 +nostr-pub.wellorder.net,45.5201,-122.99 +nostr.twinkle.lol,51.902,7.6657 +nostr.carroarmato0.be,50.914,3.21378 +bcast.girino.org,43.6532,-79.3832 +treuzkas.branruz.com,48.8575,2.35138 +dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 +relay.endfiat.money,59.3327,18.0656 +relay.jmoose.rocks,37.4419,-122.143 +relay.degmods.com,50.4754,12.3683 +relay.ditto.pub,43.6532,-79.3832 +nostrelay.circum.space:443,52.2245,8.826 +relay.pocketnostr.com,40.8054,-74.0241 +nostr.wecsats.io,43.6532,-79.3832 +node.kommonzenze.de,49.4521,11.0767 +nostr.sathoarder.com,48.5734,7.75211 +chorus.mikedilger.com:444,-36.8906,174.794 +cache.trustr.ing,43.6548,-79.3885 +bitcoiner.social,40.7608,-111.891 +relay.tdw.gg,34.0549,-118.243 +relay.edufeed.org:443,49.4521,11.0767 +relay.snotr.nl:49999,51.9758,4.31389 +nostr.bgbitcoin.club,50.4754,12.3683 +relay.bowlafterbowl.com,32.9483,-96.7299 +relay-rpi.edufeed.org,49.4521,11.0767 +x.kojira.io:443,43.6532,-79.3832 +nostr.self-determined.de,51.4379,14.2355 +schnorr.me:443,43.6532,-79.3832 +nostr.computingcache.com,45.5341,-122.956 +cs-relay.nostrdev.com,50.4754,12.3683 +relay2.veganostr.com,60.1699,24.9384 +relay.phostrich.com,40.55,-74.4758 +relay.nostrian-conquest.com,41.223,-111.974 +relay.flashapp.me,43.6548,-79.3885 +relay.yoinekodo.jp,43.6532,-79.3832 +relay-arg.zombi.cloudrodion.com,1.35208,103.82 +freelay.sovbit.host,60.1699,24.9384 +nostr.n7ekb.net,47.4941,-122.294 +purplerelay.com:443,43.6532,-79.3832 +nostr.tbxnetworx.de,47.7228,9.96456 +relay.bnos.space:443,43.6532,-79.3832 +relay.satsapp.me,50.4754,12.3683 +nostr-01.uid.ovh,50.9871,2.12554 +tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 +nostr.mom,50.4754,12.3683 +no.str.cr,8.96171,-83.5246 +nostr.spicyz.io,43.6532,-79.3832 +memlay.v0l.io,53.3498,-6.26031 +bucket.coracle.social,37.7775,-122.397 +nostr.azzamo.net,52.2633,21.0283 +relay.lanacoin-eternity.com,40.8302,-74.1299 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +conduitl2.fly.dev,38.7946,-106.535 +relay.wasabiwallet.io,43.6532,-79.3832 +portal-relay.pareto.space,49.0291,8.35696 +strfry.ymir.cloud,43.6532,-79.3832 +nostr.hoppe-relay.it.com,42.8864,-78.8784 +nostr.red5d.dev,43.6532,-79.3832 +strfry.shock.network,39.0438,-77.4874 +relay.chorus.community,48.5333,10.7 +relay.mitchelltribe.com:443,39.0438,-77.4874 +relay.olas.app,60.1699,24.9384 +testnet-relay.samt.st:443,40.8302,-74.1299 +relay.dwadziesciajeden.pl,52.2297,21.0122 +relay.gulugulu.moe,43.6532,-79.3832 +relay.underorion.se,50.1109,8.68213 +relay.mccormick.cx,52.3563,4.95714 +top.testrelay.top,43.6532,-79.3832 +nostr-02.uid.ovh,50.9871,2.12554 +nostr.relay.hedwig.sh,60.1699,24.9384 +nostr-01.yakihonne.com,1.32123,103.695 +relay.nostr.net,43.6532,-79.3832 +nostr-relay.cbrx.io,43.6532,-79.3832 +nrs-01.darkcloudarcade.com,39.0997,-94.5786 +relay.trotters.cc:443,43.6532,-79.3832 +strfry.bonsai.com,39.0438,-77.4874 +relay.snort.social,53.3498,-6.26031 +nostr.hekster.org,37.3986,-121.964 +relay.mleku.dev,32.7767,-96.797 +nostr-relay.xbytez.io:443,50.6924,3.20113 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +nostr.davenov.com,50.1109,8.68213 +relay.chatbett.de,40.7128,-74.006 +relayone.geektank.ai:443,39.0997,-94.5786 +insta-relay.apps3.slidestr.net,40.4167,-3.70329 +nostr.mom:443,50.4754,12.3683 +nostr-relay.corb.net:443,39.6478,-104.988 +relay.gulugulu.moe:443,43.6532,-79.3832 +nostr.thebiglake.org,32.71,-96.6745 +relay.nearhood.co.uk,51.5134,-0.0890675 +relay1.nostrchat.io,60.1699,24.9384 +rele.speyhard.fi,50.1109,8.68213 +relay.bornheimer.app,50.1109,8.68213 +nostr.fullstackcash.net,45.5201,-122.99 +relay2.fiatdenier.com,50.1013,8.62643 +relay.solife.me,43.6532,-79.3832 +syb.lol,34.0549,-118.243 +dev-relay.nostreon.com,60.1699,24.9384 +relay.agorist.space:443,52.3734,4.89406 +nostr.easycryptosend.it,43.6532,-79.3832 +x.kojira.io,43.6532,-79.3832 +adre.su,59.9311,30.3609 +offchain.bostr.online,43.6532,-79.3832 +relay.libernet.app,43.6532,-79.3832 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +nostr.rtvslawenia.com,49.4543,11.0746 +relay.layer.systems,49.0291,8.35695 +relay.arx-ccn.com,50.4754,12.3683 +staging.yabu.me,35.6092,139.73 +relay.vrtmrz.net,43.6532,-79.3832 +nostr.bitcoiner.social,40.7608,-111.891 +nostr.88mph.life,52.1941,-2.21905 +relay.pprgb.app,52.52,13.405 +relay.aarpia.com,37.3986,-121.964 +nostrcity-club.fly.dev,38.7946,-106.535 +buzz.ac2n-share.kozow.com,45.764,4.83566 +relay.fckstate.net,59.3293,18.0686 +nostr.thalheim.io:443,60.1699,24.9384 +nostr.carroarmato0.be:443,50.914,3.21378 +nostrcity-club.fly.dev:443,38.7946,-106.535 +relay.routstr.com,59.4016,17.9455 +nostr-dev.wellorder.net,45.5201,-122.99 +relay.directsponsor.net,42.8864,-78.8784 +relay.mwaters.net,50.9871,2.12554 +relay.littlebitstudios.com,43.6532,-79.3832 +relay.ohstr.com,43.6532,-79.3832 +testnet-relay.samt.st,40.8302,-74.1299 +relay.bullishbounty.com,43.6532,-79.3832 +relay.nostrmap.net:443,60.1699,24.9384 +relay.piazza.today,48.122,11.589 From 6bdcd46b33de4bbcb0c38aadee5688217a7c151a Mon Sep 17 00:00:00 2001 From: Taksh Date: Mon, 24 Aug 2026 21:32:36 +0530 Subject: [PATCH 19/45] fix(nostr): keep retrying relays instead of giving up on them forever A relay that fails is retried on an exponential backoff, then abandoned for the lifetime of the process. Nothing brings it back: the relay layer registers no connectivity callback, the periodic subscription validator only repairs subscriptions on sockets that are already open (and returns immediately when connectedRelayCount is 0, which is exactly the state after an outage), and connect() runs once from NostrClient.initialize(). The remaining paths that reset reconnectAttempts are a manual retry, a Tor state change, and a successful open. Two ways a relay died permanently: - Any error whose message mentioned DNS returned before scheduling anything at all. "Unable to resolve host" is what this device reports when it simply has no network, so a moment in a tunnel killed every relay at once, with no retry ever. - Otherwise the schedule stopped at MAX_RECONNECT_ATTEMPTS. With INITIAL=1s and MULTIPLIER=2 that is nine waits totalling about eight and a half minutes, after which the relay was dead. MAX_BACKOFF_INTERVAL was unreachable: attempt 9 asks for 256s and attempt 10 gave up, so the five-minute ceiling the constant defines never applied to anything. Let the backoff saturate at MAX_BACKOFF_INTERVAL and keep retrying there. A name-resolution failure now backs off like any other error. Steady state costs one connection attempt per relay per five minutes; the previous behaviour cost the user every internet DM, delivery receipt and geohash channel until they noticed and restarted the app. The schedule moves into RelayReconnectPolicy so it is unit-testable without OkHttp or a Context. --- .../android/nostr/NostrRelayManager.kt | 47 ++------- .../android/nostr/RelayReconnectPolicy.kt | 45 +++++++++ .../android/nostr/RelayReconnectPolicyTest.kt | 96 +++++++++++++++++++ 3 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/nostr/RelayReconnectPolicy.kt create mode 100644 app/src/test/kotlin/com/bitchat/android/nostr/RelayReconnectPolicyTest.kt diff --git a/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt b/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt index 772e2a60..81c1cac6 100644 --- a/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt +++ b/app/src/main/java/com/bitchat/android/nostr/NostrRelayManager.kt @@ -14,8 +14,6 @@ import okhttp3.* import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.min -import kotlin.math.pow /** * Manages WebSocket connections to Nostr relays @@ -48,12 +46,8 @@ class NostrRelayManager private constructor() { "wss://nostr21.com" ) - // Exponential backoff configuration (same as iOS) - private const val INITIAL_BACKOFF_INTERVAL = com.bitchat.android.util.AppConstants.Nostr.INITIAL_BACKOFF_INTERVAL_MS // 1 second - private const val MAX_BACKOFF_INTERVAL = com.bitchat.android.util.AppConstants.Nostr.MAX_BACKOFF_INTERVAL_MS // 5 minutes - private const val BACKOFF_MULTIPLIER = com.bitchat.android.util.AppConstants.Nostr.BACKOFF_MULTIPLIER - private const val MAX_RECONNECT_ATTEMPTS = com.bitchat.android.util.AppConstants.Nostr.MAX_RECONNECT_ATTEMPTS - + // Reconnect backoff lives in RelayReconnectPolicy. + // Track gift-wraps we initiated for logging private val pendingGiftWrapIDs = ConcurrentHashMap.newKeySet() @@ -1018,36 +1012,15 @@ class NostrRelayManager private constructor() { if (!desiredConnected.get() || !isNetworkActionAllowed(connectionToken) ) return - - // Check if this is a DNS error - val errorMessage = error.message?.lowercase() ?: "" - if (errorMessage.contains("hostname could not be found") || - errorMessage.contains("dns") || - errorMessage.contains("unable to resolve host")) { - - val relay = relaysList.find { it.url == relayUrl } - if (relay?.lastError == null) { - Log.w(TAG, "Nostr relay DNS failure; not retrying") - } - return - } - - // Implement exponential backoff for non-DNS errors + + // Every failure backs off and retries, including a name-resolution + // failure: "unable to resolve host" is what this device reports when it + // simply has no network, so treating it as permanent turns a walk + // through a tunnel into a dead relay layer for the rest of the process. val relay = relaysList.find { it.url == relayUrl } ?: return - relay.reconnectAttempts++ - - // Stop attempting after max attempts - if (relay.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { - Log.w(TAG, "Max Nostr relay reconnection attempts reached") - return - } - - // Calculate backoff interval - val backoffInterval = min( - INITIAL_BACKOFF_INTERVAL * BACKOFF_MULTIPLIER.pow(relay.reconnectAttempts - 1.0), - MAX_BACKOFF_INTERVAL.toDouble() - ).toLong() - + relay.reconnectAttempts = RelayReconnectPolicy.nextAttempt(relay.reconnectAttempts) + val backoffInterval = RelayReconnectPolicy.backoffMs(relay.reconnectAttempts) + relay.nextReconnectTime = System.currentTimeMillis() + backoffInterval Log.d(TAG, "Scheduling Nostr relay reconnection") diff --git a/app/src/main/java/com/bitchat/android/nostr/RelayReconnectPolicy.kt b/app/src/main/java/com/bitchat/android/nostr/RelayReconnectPolicy.kt new file mode 100644 index 00000000..199e0dcf --- /dev/null +++ b/app/src/main/java/com/bitchat/android/nostr/RelayReconnectPolicy.kt @@ -0,0 +1,45 @@ +package com.bitchat.android.nostr + +import com.bitchat.android.util.AppConstants +import kotlin.math.min +import kotlin.math.pow + +/** + * Reconnect schedule for a Nostr relay socket. + * + * A phone loses its data connection constantly — airplane mode, a tunnel, a + * Wi-Fi to cellular handover, a dead zone — and every relay fails at once when + * it does. The schedule therefore has to survive an outage of arbitrary length + * and heal on its own, because nothing else will: the relay layer registers no + * connectivity callback, the periodic subscription validator only repairs + * subscriptions on sockets that are already open, and `connect()` runs once at + * startup. + * + * So the backoff grows exponentially and then *saturates* rather than + * terminating. Retrying forever at the ceiling costs one connection attempt per + * relay per [AppConstants.Nostr.MAX_BACKOFF_INTERVAL_MS]; giving up costs the + * user every internet DM, delivery receipt and geohash channel until they + * notice and restart the app. + */ +internal object RelayReconnectPolicy { + + /** + * Attempt count past which the interval stops growing. Beyond this the + * exponential term is already above the ceiling, so pinning it keeps the + * schedule at a steady [AppConstants.Nostr.MAX_BACKOFF_INTERVAL_MS] and + * keeps the exponent from running away over a long outage. + */ + const val SATURATION_ATTEMPTS: Int = AppConstants.Nostr.MAX_RECONNECT_ATTEMPTS + + /** Attempt number to record after a failure at [previousAttempts]. */ + fun nextAttempt(previousAttempts: Int): Int = + (previousAttempts.coerceAtLeast(0) + 1).coerceAtMost(SATURATION_ATTEMPTS) + + /** Delay before the reconnect for a given attempt number (1-based). */ + fun backoffMs(attempt: Int): Long { + val step = attempt.coerceAtLeast(1) + val exponential = AppConstants.Nostr.INITIAL_BACKOFF_INTERVAL_MS * + AppConstants.Nostr.BACKOFF_MULTIPLIER.pow(step - 1.0) + return min(exponential, AppConstants.Nostr.MAX_BACKOFF_INTERVAL_MS.toDouble()).toLong() + } +} diff --git a/app/src/test/kotlin/com/bitchat/android/nostr/RelayReconnectPolicyTest.kt b/app/src/test/kotlin/com/bitchat/android/nostr/RelayReconnectPolicyTest.kt new file mode 100644 index 00000000..f5e2581a --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/nostr/RelayReconnectPolicyTest.kt @@ -0,0 +1,96 @@ +package com.bitchat.android.nostr + +import com.bitchat.android.util.AppConstants +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * The relay layer has no connectivity callback, its periodic validator only + * repairs subscriptions on sockets that are already open, and `connect()` runs + * once at startup. The backoff schedule is therefore the only thing that can + * bring relays back after the phone loses its data connection, so it has to + * saturate rather than terminate. + */ +class RelayReconnectPolicyTest { + + private val initial = AppConstants.Nostr.INITIAL_BACKOFF_INTERVAL_MS + private val ceiling = AppConstants.Nostr.MAX_BACKOFF_INTERVAL_MS + + @Test + fun `the first retry waits the initial interval`() { + assertEquals(initial, RelayReconnectPolicy.backoffMs(RelayReconnectPolicy.nextAttempt(0))) + } + + @Test + fun `the interval doubles per attempt until it reaches the ceiling`() { + var attempt = 0 + var previous = 0L + var sawCeiling = false + + repeat(RelayReconnectPolicy.SATURATION_ATTEMPTS) { + attempt = RelayReconnectPolicy.nextAttempt(attempt) + val delay = RelayReconnectPolicy.backoffMs(attempt) + + assertTrue("delay must never exceed the ceiling", delay <= ceiling) + if (delay == ceiling) { + sawCeiling = true + } else { + assertEquals("expected doubling below the ceiling", maxOf(initial, previous * 2), delay) + } + previous = delay + } + + assertTrue("the schedule must actually reach the ceiling", sawCeiling) + } + + @Test + fun `an outage longer than the schedule keeps retrying at the ceiling`() { + var attempt = 0 + // Far past the old give-up point; a real outage can last hours. + repeat(500) { attempt = RelayReconnectPolicy.nextAttempt(attempt) } + + assertEquals(RelayReconnectPolicy.SATURATION_ATTEMPTS, attempt) + assertEquals(ceiling, RelayReconnectPolicy.backoffMs(attempt)) + } + + @Test + fun `a long outage cannot run the attempt counter or the exponent away`() { + var attempt = 0 + repeat(10_000) { attempt = RelayReconnectPolicy.nextAttempt(attempt) } + + val delay = RelayReconnectPolicy.backoffMs(attempt) + assertTrue("delay must stay finite and bounded", delay in 1..ceiling) + } + + @Test + fun `a successful connection resets the schedule to the initial interval`() { + var attempt = 0 + repeat(6) { attempt = RelayReconnectPolicy.nextAttempt(attempt) } + assertTrue(RelayReconnectPolicy.backoffMs(attempt) > initial) + + // updateRelayStatus zeroes reconnectAttempts on a successful open. + attempt = 0 + + assertEquals(initial, RelayReconnectPolicy.backoffMs(RelayReconnectPolicy.nextAttempt(attempt))) + } + + @Test + fun `a nonsensical stored attempt count still yields a usable delay`() { + assertEquals(initial, RelayReconnectPolicy.backoffMs(RelayReconnectPolicy.nextAttempt(-5))) + assertTrue(RelayReconnectPolicy.backoffMs(0) in 1..ceiling) + assertTrue(RelayReconnectPolicy.backoffMs(Int.MAX_VALUE) in 1..ceiling) + } + + @Test + fun `the whole schedule stays under an hour of total wait before the ceiling`() { + var attempt = 0 + var total = 0L + repeat(RelayReconnectPolicy.SATURATION_ATTEMPTS) { + attempt = RelayReconnectPolicy.nextAttempt(attempt) + total += RelayReconnectPolicy.backoffMs(attempt) + } + + assertTrue("reaching the steady state must not take an hour", total < 60 * 60 * 1000L) + } +} From 5852fca9b5a12837ec2ac8a4b4b5a1f5e31cac7c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:38:26 +0200 Subject: [PATCH 20/45] Fix Wear content clipping on watch shapes --- wear/build.gradle.kts | 4 +- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 11 ++-- .../com/bitchat/watch/ui/PeerDebugScreen.kt | 5 +- .../java/com/bitchat/watch/ui/PeopleScreen.kt | 5 +- .../com/bitchat/watch/ui/UserDetailScreen.kt | 11 ++-- .../watch/ui/VerificationCodeScreen.kt | 11 ++-- .../bitchat/watch/ui/WearContentPadding.kt | 52 +++++++++++++++++++ .../watch/ui/WearContentPaddingTest.kt | 40 ++++++++++++++ 8 files changed, 124 insertions(+), 15 deletions(-) create mode 100644 wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt create mode 100644 wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index e177c3e2..9bd87331 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -19,8 +19,8 @@ android { targetSdk = libs.versions.targetSdk.get().toInt() // Wear releases use a separate high range because Play requires every artifact in // one application ID to have a unique version code across all form factors. - versionCode = 1_000_000_002 - versionName = "0.1.1" + versionCode = 1_000_000_003 + versionName = "0.1.2" vectorDrawables { useSupportLibrary = true diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 0bcdfed3..9f7b54f4 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -28,6 +27,7 @@ import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.TransformingLazyColumn @@ -256,7 +256,8 @@ private fun ChatBody( } } ) { - ScreenScaffold(scrollState = columnState) { + ScreenScaffold(scrollState = columnState) { scaffoldPadding -> + val layoutDirection = LocalLayoutDirection.current TransformingLazyColumn( state = columnState, modifier = Modifier.fillMaxSize(), @@ -265,7 +266,11 @@ private fun ChatBody( // The padding reserves permanent room for the floating header and action // bar; being constant, it never disturbs an in-flight scroll gesture. verticalArrangement = Arrangement.Bottom, - contentPadding = PaddingValues(top = 30.dp, bottom = 64.dp) + contentPadding = scaffoldPadding.withMinimumVerticalPadding( + layoutDirection = layoutDirection, + top = 30.dp, + bottom = 64.dp + ) ) { if (messages.isEmpty()) { item { diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt index 1994f976..c91a220e 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt @@ -46,10 +46,11 @@ fun PeerDebugScreen() { val rssi = mesh?.getPeerRSSI() ?: emptyMap() val identityRevision by WearPeerIdentityState.revision.collectAsState() - ScreenScaffold(scrollState = listState) { + ScreenScaffold(scrollState = listState) { contentPadding -> ScalingLazyColumn( state = listState, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt index 3cb86df3..b3f912f9 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt @@ -67,10 +67,11 @@ fun PeopleScreen(onOpenDm: (String) -> Unit, onEditNickname: () -> Unit) { ) } - ScreenScaffold(scrollState = listState) { + ScreenScaffold(scrollState = listState) { contentPadding -> ScalingLazyColumn( state = listState, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize(), + contentPadding = contentPadding ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt index a36c7ec6..1c2a5616 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt @@ -1,7 +1,6 @@ package com.bitchat.watch.ui import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -15,6 +14,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -48,11 +48,16 @@ fun UserDetailScreen( val listState = rememberScalingLazyListState() val palette = LocalBitchatPalette.current - ScreenScaffold(scrollState = listState) { + ScreenScaffold(scrollState = listState) { scaffoldPadding -> + val layoutDirection = LocalLayoutDirection.current ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp) + contentPadding = scaffoldPadding.withAdditionalPadding( + layoutDirection = layoutDirection, + horizontal = 10.dp, + vertical = 8.dp + ) ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt index c4c4ef0f..67517c91 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt @@ -2,7 +2,6 @@ package com.bitchat.watch.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -14,6 +13,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -43,11 +43,16 @@ fun VerificationCodeScreen(peerID: String) { val listState = rememberScalingLazyListState() val palette = LocalBitchatPalette.current - ScreenScaffold(scrollState = listState) { + ScreenScaffold(scrollState = listState) { scaffoldPadding -> + val layoutDirection = LocalLayoutDirection.current ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp) + contentPadding = scaffoldPadding.withAdditionalPadding( + layoutDirection = layoutDirection, + horizontal = 10.dp, + vertical = 8.dp + ) ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt new file mode 100644 index 00000000..28425369 --- /dev/null +++ b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt @@ -0,0 +1,52 @@ +package com.bitchat.watch.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +/** + * Preserve the responsive, shape-aware padding supplied by Wear Material while allowing a + * screen to reserve additional room for its own content or floating controls. + */ +internal fun PaddingValues.withAdditionalPadding( + layoutDirection: LayoutDirection, + horizontal: Dp = 0.dp, + vertical: Dp = 0.dp +): PaddingValues { + val start = when (layoutDirection) { + LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) + LayoutDirection.Rtl -> calculateRightPadding(layoutDirection) + } + val end = when (layoutDirection) { + LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) + LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) + } + return PaddingValues( + start = start + horizontal, + top = calculateTopPadding() + vertical, + end = end + horizontal, + bottom = calculateBottomPadding() + vertical + ) +} + +internal fun PaddingValues.withMinimumVerticalPadding( + layoutDirection: LayoutDirection, + top: Dp, + bottom: Dp +): PaddingValues { + val start = when (layoutDirection) { + LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) + LayoutDirection.Rtl -> calculateRightPadding(layoutDirection) + } + val end = when (layoutDirection) { + LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) + LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) + } + return PaddingValues( + start = start, + top = maxOf(calculateTopPadding(), top), + end = end, + bottom = maxOf(calculateBottomPadding(), bottom) + ) +} diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt new file mode 100644 index 00000000..2b16c876 --- /dev/null +++ b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt @@ -0,0 +1,40 @@ +package com.bitchat.watch.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import org.junit.Assert.assertEquals +import org.junit.Test + +class WearContentPaddingTest { + + @Test + fun `additional padding preserves scaffold insets`() { + val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp) + .withAdditionalPadding( + layoutDirection = LayoutDirection.Ltr, + horizontal = 10.dp, + vertical = 8.dp + ) + + assertEquals(20.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr)) + assertEquals(22.dp, resolved.calculateRightPadding(LayoutDirection.Ltr)) + assertEquals(26.dp, resolved.calculateTopPadding()) + assertEquals(28.dp, resolved.calculateBottomPadding()) + } + + @Test + fun `chat padding keeps responsive sides and overlay clearances`() { + val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp) + .withMinimumVerticalPadding( + layoutDirection = LayoutDirection.Ltr, + top = 30.dp, + bottom = 64.dp + ) + + assertEquals(10.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr)) + assertEquals(12.dp, resolved.calculateRightPadding(LayoutDirection.Ltr)) + assertEquals(30.dp, resolved.calculateTopPadding()) + assertEquals(64.dp, resolved.calculateBottomPadding()) + } +} From 2e41dabd07398ce614a25f7aa959b5b94616ca3b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:54:20 +0200 Subject: [PATCH 21/45] Add round-screen responsive Wear insets --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 12 +++++---- .../com/bitchat/watch/ui/PeerDebugScreen.kt | 6 +++-- .../java/com/bitchat/watch/ui/PeopleScreen.kt | 6 +++-- .../com/bitchat/watch/ui/UserDetailScreen.kt | 12 +++++---- .../watch/ui/VerificationCodeScreen.kt | 12 +++++---- .../bitchat/watch/ui/WearContentPadding.kt | 25 +++++++++++++++++++ .../watch/ui/WearContentPaddingTest.kt | 7 ++++++ 7 files changed, 61 insertions(+), 19 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 9f7b54f4..7bbbcd77 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -266,11 +266,13 @@ private fun ChatBody( // The padding reserves permanent room for the floating header and action // bar; being constant, it never disturbs an in-flight scroll gesture. verticalArrangement = Arrangement.Bottom, - contentPadding = scaffoldPadding.withMinimumVerticalPadding( - layoutDirection = layoutDirection, - top = 30.dp, - bottom = 64.dp - ) + contentPadding = scaffoldPadding + .withRoundScreenPadding(layoutDirection) + .withMinimumVerticalPadding( + layoutDirection = layoutDirection, + top = 30.dp, + bottom = 64.dp + ) ) { if (messages.isEmpty()) { item { diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt index c91a220e..bda2c2bd 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -46,11 +47,12 @@ fun PeerDebugScreen() { val rssi = mesh?.getPeerRSSI() ?: emptyMap() val identityRevision by WearPeerIdentityState.revision.collectAsState() - ScreenScaffold(scrollState = listState) { contentPadding -> + ScreenScaffold(scrollState = listState) { scaffoldPadding -> + val layoutDirection = LocalLayoutDirection.current ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = contentPadding + contentPadding = scaffoldPadding.withRoundScreenPadding(layoutDirection) ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt index b3f912f9..d8d7df17 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -67,11 +68,12 @@ fun PeopleScreen(onOpenDm: (String) -> Unit, onEditNickname: () -> Unit) { ) } - ScreenScaffold(scrollState = listState) { contentPadding -> + ScreenScaffold(scrollState = listState) { scaffoldPadding -> + val layoutDirection = LocalLayoutDirection.current ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = contentPadding + contentPadding = scaffoldPadding.withRoundScreenPadding(layoutDirection) ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt index 1c2a5616..1188fed0 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt @@ -53,11 +53,13 @@ fun UserDetailScreen( ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = scaffoldPadding.withAdditionalPadding( - layoutDirection = layoutDirection, - horizontal = 10.dp, - vertical = 8.dp - ) + contentPadding = scaffoldPadding + .withRoundScreenPadding(layoutDirection) + .withAdditionalPadding( + layoutDirection = layoutDirection, + horizontal = 10.dp, + vertical = 8.dp + ) ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt index 67517c91..89e83ee3 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt @@ -48,11 +48,13 @@ fun VerificationCodeScreen(peerID: String) { ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = scaffoldPadding.withAdditionalPadding( - layoutDirection = layoutDirection, - horizontal = 10.dp, - vertical = 8.dp - ) + contentPadding = scaffoldPadding + .withRoundScreenPadding(layoutDirection) + .withAdditionalPadding( + layoutDirection = layoutDirection, + horizontal = 10.dp, + vertical = 8.dp + ) ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt index 28425369..0419bdfa 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt @@ -1,9 +1,12 @@ package com.bitchat.watch.ui import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import kotlin.math.ceil /** * Preserve the responsive, shape-aware padding supplied by Wear Material while allowing a @@ -50,3 +53,25 @@ internal fun PaddingValues.withMinimumVerticalPadding( bottom = maxOf(calculateBottomPadding(), bottom) ) } + +@Composable +internal fun PaddingValues.withRoundScreenPadding( + layoutDirection: LayoutDirection +): PaddingValues = withAdditionalPadding( + layoutDirection = layoutDirection, + horizontal = additionalRoundScreenPadding( + screenWidthDp = LocalConfiguration.current.screenWidthDp, + isScreenRound = LocalConfiguration.current.isScreenRound + ) +) + +internal fun additionalRoundScreenPadding( + screenWidthDp: Int, + isScreenRound: Boolean +): Dp = if (isScreenRound) { + ceil(screenWidthDp * ROUND_SCREEN_ADDITIONAL_PADDING_FRACTION).toInt().dp +} else { + 0.dp +} + +private const val ROUND_SCREEN_ADDITIONAL_PADDING_FRACTION = 0.10f diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt index 2b16c876..3fe5273b 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt @@ -37,4 +37,11 @@ class WearContentPaddingTest { assertEquals(30.dp, resolved.calculateTopPadding()) assertEquals(64.dp, resolved.calculateBottomPadding()) } + + @Test + fun `round screens receive an additional ten percent inset`() { + assertEquals(20.dp, additionalRoundScreenPadding(192, isScreenRound = true)) + assertEquals(22.dp, additionalRoundScreenPadding(220, isScreenRound = true)) + assertEquals(0.dp, additionalRoundScreenPadding(192, isScreenRound = false)) + } } From 7788328917eacb7de1a9d16084975e8ec075077d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:58:03 +0200 Subject: [PATCH 22/45] Keep Wear chat rows clear of controls --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 14 ++++++------- .../bitchat/watch/ui/WearContentPadding.kt | 21 ------------------- .../watch/ui/WearContentPaddingTest.kt | 15 ------------- 3 files changed, 6 insertions(+), 44 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 7bbbcd77..640eb8cd 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -260,19 +260,17 @@ private fun ChatBody( val layoutDirection = LocalLayoutDirection.current TransformingLazyColumn( state = columnState, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(top = 30.dp, bottom = 64.dp), // Arrangement.Bottom anchors short content to the bottom: the first message // starts just above the action bar and new messages push history upward. - // The padding reserves permanent room for the floating header and action - // bar; being constant, it never disturbs an in-flight scroll gesture. + // The viewport itself stays between the floating header and action bar, so + // rows cannot scroll underneath either control. Its geometry is constant, + // so showing or hiding the overlays never disturbs an in-flight gesture. verticalArrangement = Arrangement.Bottom, contentPadding = scaffoldPadding .withRoundScreenPadding(layoutDirection) - .withMinimumVerticalPadding( - layoutDirection = layoutDirection, - top = 30.dp, - bottom = 64.dp - ) ) { if (messages.isEmpty()) { item { diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt index 0419bdfa..02737bce 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt @@ -33,27 +33,6 @@ internal fun PaddingValues.withAdditionalPadding( ) } -internal fun PaddingValues.withMinimumVerticalPadding( - layoutDirection: LayoutDirection, - top: Dp, - bottom: Dp -): PaddingValues { - val start = when (layoutDirection) { - LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) - LayoutDirection.Rtl -> calculateRightPadding(layoutDirection) - } - val end = when (layoutDirection) { - LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) - LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) - } - return PaddingValues( - start = start, - top = maxOf(calculateTopPadding(), top), - end = end, - bottom = maxOf(calculateBottomPadding(), bottom) - ) -} - @Composable internal fun PaddingValues.withRoundScreenPadding( layoutDirection: LayoutDirection diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt index 3fe5273b..055f062c 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt @@ -23,21 +23,6 @@ class WearContentPaddingTest { assertEquals(28.dp, resolved.calculateBottomPadding()) } - @Test - fun `chat padding keeps responsive sides and overlay clearances`() { - val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp) - .withMinimumVerticalPadding( - layoutDirection = LayoutDirection.Ltr, - top = 30.dp, - bottom = 64.dp - ) - - assertEquals(10.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr)) - assertEquals(12.dp, resolved.calculateRightPadding(LayoutDirection.Ltr)) - assertEquals(30.dp, resolved.calculateTopPadding()) - assertEquals(64.dp, resolved.calculateBottomPadding()) - } - @Test fun `round screens receive an additional ten percent inset`() { assertEquals(20.dp, additionalRoundScreenPadding(192, isScreenRound = true)) From 5d66b69129ec0a496bf6ad86be11b4631fc9ee55 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:01:46 +0200 Subject: [PATCH 23/45] Clip Wear messages below the chat header --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 640eb8cd..f30788a7 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -6,10 +6,12 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -20,6 +22,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer @@ -35,6 +38,7 @@ import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.lazy.rememberTransformationSpec import androidx.wear.compose.material3.lazy.transformedHeight @@ -262,7 +266,8 @@ private fun ChatBody( state = columnState, modifier = Modifier .fillMaxSize() - .padding(top = 30.dp, bottom = 64.dp), + .padding(top = 30.dp, bottom = 64.dp) + .clipToBounds(), // Arrangement.Bottom anchors short content to the bottom: the first message // starts just above the action bar and new messages push history upward. // The viewport itself stays between the floating header and action bar, so @@ -304,7 +309,14 @@ private fun ChatBody( // The header stays put and shrinks to its dense form instead of disappearing; // as an overlay its size animation never touches the list's scroll geometry. - Box(modifier = Modifier.align(Alignment.TopCenter)) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .height(30.dp) + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.TopCenter + ) { header(controlsVisible) } From 4a1de47ed64f33e614fe7395bf2a7f0ead6e1dca Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:04:46 +0200 Subject: [PATCH 24/45] Cover transformed Wear header boundary --- wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index f30788a7..19e11bd3 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -313,7 +313,9 @@ private fun ChatBody( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() - .height(30.dp) + // Overlap the clipped list viewport by 4dp so transformed glyphs cannot + // leave antialiased remnants exactly on the boundary at larger font scales. + .height(34.dp) .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.TopCenter ) { From f7440383182ce4c42a1f09985afd8648ad638ea5 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:08:44 +0200 Subject: [PATCH 25/45] Preserve width for Wear Material cards --- wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt | 4 +--- wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt | 4 +--- wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt | 1 - .../main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt | 1 - 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt index bda2c2bd..fa83e59b 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -48,11 +47,10 @@ fun PeerDebugScreen() { val identityRevision by WearPeerIdentityState.revision.collectAsState() ScreenScaffold(scrollState = listState) { scaffoldPadding -> - val layoutDirection = LocalLayoutDirection.current ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = scaffoldPadding.withRoundScreenPadding(layoutDirection) + contentPadding = scaffoldPadding ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt index d8d7df17..a27fab14 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeopleScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -69,11 +68,10 @@ fun PeopleScreen(onOpenDm: (String) -> Unit, onEditNickname: () -> Unit) { } ScreenScaffold(scrollState = listState) { scaffoldPadding -> - val layoutDirection = LocalLayoutDirection.current ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), - contentPadding = scaffoldPadding.withRoundScreenPadding(layoutDirection) + contentPadding = scaffoldPadding ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt index 1188fed0..75f2db9c 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt @@ -54,7 +54,6 @@ fun UserDetailScreen( state = listState, modifier = Modifier.fillMaxSize(), contentPadding = scaffoldPadding - .withRoundScreenPadding(layoutDirection) .withAdditionalPadding( layoutDirection = layoutDirection, horizontal = 10.dp, diff --git a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt index 89e83ee3..104ab660 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt @@ -49,7 +49,6 @@ fun VerificationCodeScreen(peerID: String) { state = listState, modifier = Modifier.fillMaxSize(), contentPadding = scaffoldPadding - .withRoundScreenPadding(layoutDirection) .withAdditionalPadding( layoutDirection = layoutDirection, horizontal = 10.dp, From 45915fab8fdd8e5295e695cdeb338a5972987fd3 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:15:17 +0200 Subject: [PATCH 26/45] Tune Wear header boundary by screen shape --- wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 19e11bd3..ffc3b0cf 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign @@ -189,6 +190,7 @@ private fun ChatBody( val palette = LocalBitchatPalette.current val context = LocalContext.current val transformationSpec = rememberTransformationSpec() + val headerBackdropHeight = if (LocalConfiguration.current.isScreenRound) 35.dp else 37.dp // Slide-to-cancel: while recording, the finger's position is tracked globally; the // overlay's mic button reports its bounds and becomes the cancel target when the @@ -313,9 +315,9 @@ private fun ChatBody( modifier = Modifier .align(Alignment.TopCenter) .fillMaxWidth() - // Overlap the clipped list viewport by 4dp so transformed glyphs cannot - // leave antialiased remnants exactly on the boundary at larger font scales. - .height(34.dp) + // Overlap the clipped list viewport enough for each shape so transformed + // glyphs cannot leave antialiased remnants exactly on the boundary. + .height(headerBackdropHeight) .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.TopCenter ) { From 34f94882f5d2077f33b863101e48a5f79f3cb595 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:57:24 +0200 Subject: [PATCH 27/45] Restore Wear chat content space --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 8 ++-- .../bitchat/watch/ui/WearContentPadding.kt | 38 ++++++++----------- .../watch/ui/WearContentPaddingTest.kt | 12 ++++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index ffc3b0cf..eb0b75ef 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -190,7 +190,7 @@ private fun ChatBody( val palette = LocalBitchatPalette.current val context = LocalContext.current val transformationSpec = rememberTransformationSpec() - val headerBackdropHeight = if (LocalConfiguration.current.isScreenRound) 35.dp else 37.dp + val headerBackdropHeight = if (LocalConfiguration.current.isScreenRound) 49.dp else 37.dp // Slide-to-cancel: while recording, the finger's position is tracked globally; the // overlay's mic button reports its bounds and becomes the cancel target when the @@ -276,8 +276,10 @@ private fun ChatBody( // rows cannot scroll underneath either control. Its geometry is constant, // so showing or hiding the overlays never disturbs an in-flight gesture. verticalArrangement = Arrangement.Bottom, - contentPadding = scaffoldPadding - .withRoundScreenPadding(layoutDirection) + // Wear Material already supplies a responsive 5.2% horizontal inset. Keep it, + // but omit the scaffold's vertical inset because the viewport itself reserves + // the header and action-bar space. Stacking both made chat narrow and too high. + contentPadding = scaffoldPadding.horizontalOnly(layoutDirection) ) { if (messages.isEmpty()) { item { diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt index 02737bce..765b9a6f 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt @@ -1,12 +1,9 @@ package com.bitchat.watch.ui import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import kotlin.math.ceil /** * Preserve the responsive, shape-aware padding supplied by Wear Material while allowing a @@ -33,24 +30,21 @@ internal fun PaddingValues.withAdditionalPadding( ) } -@Composable -internal fun PaddingValues.withRoundScreenPadding( +/** + * Keep the responsive horizontal inset supplied by Wear Material without also reserving its + * vertical list padding. Chat owns its fixed header and action-bar clearances, so stacking the + * scaffold's vertical padding on top would unnecessarily shorten the message viewport. + */ +internal fun PaddingValues.horizontalOnly( layoutDirection: LayoutDirection -): PaddingValues = withAdditionalPadding( - layoutDirection = layoutDirection, - horizontal = additionalRoundScreenPadding( - screenWidthDp = LocalConfiguration.current.screenWidthDp, - isScreenRound = LocalConfiguration.current.isScreenRound - ) -) - -internal fun additionalRoundScreenPadding( - screenWidthDp: Int, - isScreenRound: Boolean -): Dp = if (isScreenRound) { - ceil(screenWidthDp * ROUND_SCREEN_ADDITIONAL_PADDING_FRACTION).toInt().dp -} else { - 0.dp +): PaddingValues { + val start = when (layoutDirection) { + LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) + LayoutDirection.Rtl -> calculateRightPadding(layoutDirection) + } + val end = when (layoutDirection) { + LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) + LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) + } + return PaddingValues(start = start, end = end) } - -private const val ROUND_SCREEN_ADDITIONAL_PADDING_FRACTION = 0.10f diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt index 055f062c..c9536ba0 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt @@ -24,9 +24,13 @@ class WearContentPaddingTest { } @Test - fun `round screens receive an additional ten percent inset`() { - assertEquals(20.dp, additionalRoundScreenPadding(192, isScreenRound = true)) - assertEquals(22.dp, additionalRoundScreenPadding(220, isScreenRound = true)) - assertEquals(0.dp, additionalRoundScreenPadding(192, isScreenRound = false)) + fun `horizontal only padding preserves width inset and restores vertical space`() { + val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp) + .horizontalOnly(LayoutDirection.Ltr) + + assertEquals(10.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr)) + assertEquals(12.dp, resolved.calculateRightPadding(LayoutDirection.Ltr)) + assertEquals(0.dp, resolved.calculateTopPadding()) + assertEquals(0.dp, resolved.calculateBottomPadding()) } } From ed617f0f1b3641255a22ca24dca70dc15df629a1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:13:07 +0200 Subject: [PATCH 28/45] Fix Wear chat autoscroll and focal scaling --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 88 ++++++++++++++----- .../bitchat/watch/ui/WearContentPadding.kt | 14 +-- .../bitchat/watch/ui/ChatAutoScrollTest.kt | 40 +++++++++ .../watch/ui/WearContentPaddingTest.kt | 12 ++- 4 files changed, 120 insertions(+), 34 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index eb0b75ef..d373590e 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -22,10 +22,11 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput @@ -87,35 +88,41 @@ fun ChatScaffold( previousCount = messages.size } - // One state drives both overlays: visible at the bottom or when scrolling toward it, - // hidden when scrolling up into history. The 24px (~12dp) threshold is deliberately - // small so the controls answer every flick immediately. - var atNewest by remember { mutableStateOf(true) } + // Follow intent is changed only by an actual user scroll away from the newest item or by + // reaching the end again. A new item temporarily makes canScrollForward true before layout; + // treating that transient range change as user intent breaks automatic following. + var followNewest by remember { mutableStateOf(true) } val controlsVisible = remember { mutableStateOf(true) } LaunchedEffect(columnState) { var lastPosition = -1 snapshotFlow { val first = columnState.layoutInfo.visibleItems.firstOrNull() - Triple(columnState.canScrollForward, first?.index ?: 0, first?.offset ?: 0) - }.collect { (canScrollForward, index, offset) -> - val position = index * 100_000 + offset - atNewest = !canScrollForward - if (!canScrollForward) { + ChatScrollSnapshot( + canScrollForward = columnState.canScrollForward, + isScrollInProgress = columnState.isScrollInProgress, + position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0) + ) + }.collect { snapshot -> + followNewest = updatedFollowNewest( + current = followNewest, + snapshot = snapshot, + previousPosition = lastPosition + ) + if (!snapshot.canScrollForward) { controlsVisible.value = true } else if (lastPosition >= 0) { when { - position > lastPosition + 24 -> controlsVisible.value = true - position < lastPosition - 24 -> controlsVisible.value = false + snapshot.position > lastPosition + CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> + controlsVisible.value = true + snapshot.position < lastPosition - CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> + controlsVisible.value = false } } - lastPosition = position + lastPosition = snapshot.position } } - // Stick to bottom: follow new messages while resting at the newest. - // Capture this when the message count changes, before the new layout can temporarily make - // canScrollForward true and report that the user is browsing history. - val followNewest = remember(messages.size) { atNewest } + // Stick to bottom when the user has not intentionally moved into history. LaunchedEffect(columnState, messages.size) { if (messages.isNotEmpty() && followNewest) { val expectedSingleMessageKey = messages.singleOrNull()?.id @@ -160,6 +167,24 @@ internal data class MeasuredChatLayout( val singleVisibleItemKey: Any? ) +internal data class ChatScrollSnapshot( + val canScrollForward: Boolean, + val isScrollInProgress: Boolean, + val position: Int +) + +internal fun updatedFollowNewest( + current: Boolean, + snapshot: ChatScrollSnapshot, + previousPosition: Int +): Boolean = when { + !snapshot.canScrollForward -> true + snapshot.isScrollInProgress && + previousPosition >= 0 && + snapshot.position < previousPosition - CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> false + else -> current +} + internal suspend fun scrollToNewestAfterItemsMeasured( expectedItemCount: Int, expectedSingleMessageKey: Any?, @@ -268,18 +293,30 @@ private fun ChatBody( state = columnState, modifier = Modifier .fillMaxSize() - .padding(top = 30.dp, bottom = 64.dp) - .clipToBounds(), + // Keep full-screen measurement so Wear's transformation focal point and + // scroll range stay correct. Only drawing is clipped around the overlays. + .drawWithContent { + clipRect( + top = headerBackdropHeight.toPx(), + bottom = size.height - CHAT_ACTION_BAR_CLEARANCE.toPx() + ) { + this@drawWithContent.drawContent() + } + }, // Arrangement.Bottom anchors short content to the bottom: the first message // starts just above the action bar and new messages push history upward. // The viewport itself stays between the floating header and action bar, so - // rows cannot scroll underneath either control. Its geometry is constant, - // so showing or hiding the overlays never disturbs an in-flight gesture. + // content stays clear of both controls while the full-screen list geometry keeps + // autoscroll and the native transformation focal point intact. verticalArrangement = Arrangement.Bottom, // Wear Material already supplies a responsive 5.2% horizontal inset. Keep it, - // but omit the scaffold's vertical inset because the viewport itself reserves - // the header and action-bar space. Stacking both made chat narrow and too high. - contentPadding = scaffoldPadding.horizontalOnly(layoutDirection) + // while replacing its vertical inset with the overlay clearances used before the + // shape fix. This avoids both duplicated padding and a shortened list viewport. + contentPadding = scaffoldPadding.withVerticalClearance( + layoutDirection = layoutDirection, + top = CHAT_HEADER_CONTENT_CLEARANCE, + bottom = CHAT_ACTION_BAR_CLEARANCE + ) ) { if (messages.isEmpty()) { item { @@ -356,6 +393,9 @@ private fun ChatBody( // Extra finger slack (px, ~28dp at watch density) around the cancel target so the snap // engages as the finger approaches, not only on exact contact. private const val CANCEL_HOVER_SLANT_PX = 56f +private const val CHAT_SCROLL_DIRECTION_THRESHOLD_PX = 24 +private val CHAT_HEADER_CONTENT_CLEARANCE = 30.dp +private val CHAT_ACTION_BAR_CLEARANCE = 64.dp // Magnetic zone geometry (px at watch density): the button starts reacting at // MAGNET_OUTER_PX from its center and fully blushes at MAGNET_INNER_PX (~the activation // boundary); it leans toward the finger by up to MAGNET_PULL_PX. diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt index 765b9a6f..e24671ff 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt @@ -31,12 +31,14 @@ internal fun PaddingValues.withAdditionalPadding( } /** - * Keep the responsive horizontal inset supplied by Wear Material without also reserving its - * vertical list padding. Chat owns its fixed header and action-bar clearances, so stacking the - * scaffold's vertical padding on top would unnecessarily shorten the message viewport. + * Keep the responsive horizontal inset supplied by Wear Material while replacing its vertical + * padding with screen-owned clearances. This keeps the lazy list full-screen for correct Wear + * transformation and scroll calculations without stacking duplicate vertical insets. */ -internal fun PaddingValues.horizontalOnly( - layoutDirection: LayoutDirection +internal fun PaddingValues.withVerticalClearance( + layoutDirection: LayoutDirection, + top: Dp, + bottom: Dp ): PaddingValues { val start = when (layoutDirection) { LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) @@ -46,5 +48,5 @@ internal fun PaddingValues.horizontalOnly( LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) } - return PaddingValues(start = start, end = end) + return PaddingValues(start = start, top = top, end = end, bottom = bottom) } diff --git a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt index 0b3f85b5..ad7c687a 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt @@ -10,6 +10,46 @@ import org.junit.Test class ChatAutoScrollTest { + @Test + fun `new scroll range from appended message keeps follow intent`() { + val updated = updatedFollowNewest( + current = true, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = false, + position = 100 + ), + previousPosition = 100 + ) + + assertEquals(true, updated) + } + + @Test + fun `user scroll away disables follow until list reaches newest again`() { + val browsingHistory = updatedFollowNewest( + current = true, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = true, + position = 60 + ), + previousPosition = 100 + ) + assertFalse(browsingHistory) + + val dockedAgain = updatedFollowNewest( + current = browsingHistory, + snapshot = ChatScrollSnapshot( + canScrollForward = false, + isScrollInProgress = false, + position = 200 + ), + previousPosition = 60 + ) + assertEquals(true, dockedAgain) + } + @Test fun `newest scroll waits until appended message is measured`() = runTest { val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null)) diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt index c9536ba0..477fad77 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt @@ -24,13 +24,17 @@ class WearContentPaddingTest { } @Test - fun `horizontal only padding preserves width inset and restores vertical space`() { + fun `vertical clearance preserves responsive width and replaces scaffold height inset`() { val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp) - .horizontalOnly(LayoutDirection.Ltr) + .withVerticalClearance( + layoutDirection = LayoutDirection.Ltr, + top = 30.dp, + bottom = 64.dp + ) assertEquals(10.dp, resolved.calculateLeftPadding(LayoutDirection.Ltr)) assertEquals(12.dp, resolved.calculateRightPadding(LayoutDirection.Ltr)) - assertEquals(0.dp, resolved.calculateTopPadding()) - assertEquals(0.dp, resolved.calculateBottomPadding()) + assertEquals(30.dp, resolved.calculateTopPadding()) + assertEquals(64.dp, resolved.calculateBottomPadding()) } } From 1a9f6befd43dd854ee011991b8c642922ad4b989 Mon Sep 17 00:00:00 2001 From: Taksh Date: Tue, 25 Aug 2026 07:40:04 +0530 Subject: [PATCH 29/45] fix: destroy conversation database files when panic wipe throws If SQLite clearAll fails mid-panic, in-memory state was already cleared but a process restart could reload encrypted history from disk (#699). Fall back to deleting the database files before reporting failure. --- .../android/services/ConversationRepository.kt | 17 +++++++++++++++++ .../services/ConversationDatabaseTest.kt | 16 ++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/app/src/main/java/com/bitchat/android/services/ConversationRepository.kt b/app/src/main/java/com/bitchat/android/services/ConversationRepository.kt index 84420ec0..bc474158 100644 --- a/app/src/main/java/com/bitchat/android/services/ConversationRepository.kt +++ b/app/src/main/java/com/bitchat/android/services/ConversationRepository.kt @@ -317,6 +317,7 @@ class ConversationRepository internal constructor( true } catch (error: Exception) { Log.e(TAG, "Unable to synchronously clear private conversations", error) + database.destroyStorage() _storeState.value = ConversationStoreState.Error( error.message ?: "Unable to erase conversations" ) @@ -1097,6 +1098,22 @@ internal class ConversationDatabase( writableDatabase.rawQuery("PRAGMA incremental_vacuum", null).use { } } + /** + * Last-resort panic cleanup when [clearAll] throws: drop the encrypted database + * files so a later reload cannot resurrect erased conversations (#699). + */ + fun destroyStorage() { + try { + clearAll() + } catch (error: Exception) { + Log.e(TAG, "clearAll failed during destroyStorage; deleting database files", error) + } + try { + close() + } catch (_: Exception) { } + applicationContext.deleteDatabase(databaseName) + } + fun pruneToRetentionLimits(): Set { val db = writableDatabase val orphanedMediaPaths = linkedSetOf() diff --git a/app/src/test/kotlin/com/bitchat/android/services/ConversationDatabaseTest.kt b/app/src/test/kotlin/com/bitchat/android/services/ConversationDatabaseTest.kt index 9a01ef37..65f484a8 100644 --- a/app/src/test/kotlin/com/bitchat/android/services/ConversationDatabaseTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/services/ConversationDatabaseTest.kt @@ -433,6 +433,22 @@ class ConversationDatabaseTest { } } + @Test + fun `destroy storage deletes database files so history cannot reload`() { + database.upsertMessage( + "contact_alice", + setOf("contact_alice"), + "alice", + message("survives-failure", "alice", 1L), + true + ) + assertTrue(context.databaseList().contains(databaseName)) + + database.destroyStorage() + + assertFalse(context.databaseList().contains(databaseName)) + } + @Test fun `version one plaintext database migrates without losing history`() { database.close() From 898cfde066f4049bcfb9738ec5ef32e29c1d165e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:39:16 +0200 Subject: [PATCH 30/45] Restore Wear chat edge fading --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index d373590e..17a3101c 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -6,12 +6,10 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -22,15 +20,12 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign @@ -40,7 +35,6 @@ import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.ScreenScaffold -import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text import androidx.wear.compose.material3.lazy.rememberTransformationSpec import androidx.wear.compose.material3.lazy.transformedHeight @@ -215,8 +209,6 @@ private fun ChatBody( val palette = LocalBitchatPalette.current val context = LocalContext.current val transformationSpec = rememberTransformationSpec() - val headerBackdropHeight = if (LocalConfiguration.current.isScreenRound) 49.dp else 37.dp - // Slide-to-cancel: while recording, the finger's position is tracked globally; the // overlay's mic button reports its bounds and becomes the cancel target when the // finger hovers it (with generous slack so the snap engages on approach). @@ -291,23 +283,16 @@ private fun ChatBody( val layoutDirection = LocalLayoutDirection.current TransformingLazyColumn( state = columnState, - modifier = Modifier - .fillMaxSize() - // Keep full-screen measurement so Wear's transformation focal point and - // scroll range stay correct. Only drawing is clipped around the overlays. - .drawWithContent { - clipRect( - top = headerBackdropHeight.toPx(), - bottom = size.height - CHAT_ACTION_BAR_CLEARANCE.toPx() - ) { - this@drawWithContent.drawContent() - } - }, + // Keep the list full-screen and unclipped. Wear's transformation spec scales + // and fades rows naturally along the round display edge, including underneath + // the transparent header and floating action controls. + modifier = Modifier.fillMaxSize(), // Arrangement.Bottom anchors short content to the bottom: the first message // starts just above the action bar and new messages push history upward. - // The viewport itself stays between the floating header and action bar, so - // content stays clear of both controls while the full-screen list geometry keeps - // autoscroll and the native transformation focal point intact. + // The scroll range reserves resting space for the floating controls while the + // full-screen viewport preserves autoscroll and the native transformation focal + // point. Rows may travel behind the overlays only after they have begun the Wear + // edge scale/fade treatment. verticalArrangement = Arrangement.Bottom, // Wear Material already supplies a responsive 5.2% horizontal inset. Keep it, // while replacing its vertical inset with the overlay clearances used before the @@ -353,11 +338,7 @@ private fun ChatBody( Box( modifier = Modifier .align(Alignment.TopCenter) - .fillMaxWidth() - // Overlap the clipped list viewport enough for each shape so transformed - // glyphs cannot leave antialiased remnants exactly on the boundary. - .height(headerBackdropHeight) - .background(MaterialTheme.colorScheme.background), + .fillMaxWidth(), contentAlignment = Alignment.TopCenter ) { header(controlsVisible) From 3ed46222baf4c91c2cb7ba61920be022146408d6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:56:00 +0200 Subject: [PATCH 31/45] Fade Wear chat at screen contour --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 17a3101c..bec5125d 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -20,12 +20,18 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign @@ -209,6 +215,7 @@ private fun ChatBody( val palette = LocalBitchatPalette.current val context = LocalContext.current val transformationSpec = rememberTransformationSpec() + val isScreenRound = LocalConfiguration.current.isScreenRound // Slide-to-cancel: while recording, the finger's position is tracked globally; the // overlay's mic button reports its bounds and becomes the cancel target when the // finger hovers it (with generous slack so the snap engages on approach). @@ -283,10 +290,44 @@ private fun ChatBody( val layoutDirection = LocalLayoutDirection.current TransformingLazyColumn( state = columnState, - // Keep the list full-screen and unclipped. Wear's transformation spec scales - // and fades rows naturally along the round display edge, including underneath - // the transparent header and floating action controls. - modifier = Modifier.fillMaxSize(), + // Keep the list full-screen and geometrically unclipped. Wear's transformation + // spec curves rows along the round display; a soft destination-alpha mask then + // makes them fully transparent at the physical edges instead of cutting glyphs. + modifier = Modifier + .fillMaxSize() + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithContent { + drawContent() + val edgeMask = if (isScreenRound) { + // Fade toward the actual circular contour so glyphs become + // transparent before the panel can crop their left or right edge. + Brush.radialGradient( + 0f to Color.Black, + CHAT_ROUND_EDGE_OPAQUE_STOP to Color.Black, + 1f to Color.Transparent, + center = Offset(size.width / 2f, size.height / 2f), + radius = size.minDimension / 2f + ) + } else { + val topOpaqueStop = + (CHAT_HEADER_EDGE_FADE.toPx() / size.height).coerceIn(0f, 1f) + val bottomOpaqueStop = + (1f - CHAT_ACTION_BAR_EDGE_FADE.toPx() / size.height) + .coerceIn(topOpaqueStop, 1f) + Brush.verticalGradient( + 0f to Color.Transparent, + topOpaqueStop to Color.Black, + bottomOpaqueStop to Color.Black, + 1f to Color.Transparent + ) + } + drawRect( + brush = edgeMask, + blendMode = BlendMode.DstIn + ) + }, // Arrangement.Bottom anchors short content to the bottom: the first message // starts just above the action bar and new messages push history upward. // The scroll range reserves resting space for the floating controls while the @@ -377,6 +418,9 @@ private const val CANCEL_HOVER_SLANT_PX = 56f private const val CHAT_SCROLL_DIRECTION_THRESHOLD_PX = 24 private val CHAT_HEADER_CONTENT_CLEARANCE = 30.dp private val CHAT_ACTION_BAR_CLEARANCE = 64.dp +private val CHAT_HEADER_EDGE_FADE = 36.dp +private val CHAT_ACTION_BAR_EDGE_FADE = 72.dp +private const val CHAT_ROUND_EDGE_OPAQUE_STOP = 0.78f // Magnetic zone geometry (px at watch density): the button starts reacting at // MAGNET_OUTER_PX from its center and fully blushes at MAGNET_INNER_PX (~the activation // boundary); it leans toward the finger by up to MAGNET_PULL_PX. From a125da48aabbfedc922d298f323543ee79c54486 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:46:13 +0200 Subject: [PATCH 32/45] Polish Wear shape-aware layout implementation --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 22 +++++++------- .../com/bitchat/watch/ui/PeerDebugScreen.kt | 5 ++-- .../bitchat/watch/ui/WearContentPadding.kt | 29 +++++++++--------- .../watch/ui/WearContentPaddingTest.kt | 30 +++++++++++++++++++ 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index bec5125d..15b18436 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -20,7 +20,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.BlendMode @@ -298,8 +298,7 @@ private fun ChatBody( .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } - .drawWithContent { - drawContent() + .drawWithCache { val edgeMask = if (isScreenRound) { // Fade toward the actual circular contour so glyphs become // transparent before the panel can crop their left or right edge. @@ -323,10 +322,13 @@ private fun ChatBody( 1f to Color.Transparent ) } - drawRect( - brush = edgeMask, - blendMode = BlendMode.DstIn - ) + onDrawWithContent { + drawContent() + drawRect( + brush = edgeMask, + blendMode = BlendMode.DstIn + ) + } }, // Arrangement.Bottom anchors short content to the bottom: the first message // starts just above the action bar and new messages push history upward. @@ -335,9 +337,9 @@ private fun ChatBody( // point. Rows may travel behind the overlays only after they have begun the Wear // edge scale/fade treatment. verticalArrangement = Arrangement.Bottom, - // Wear Material already supplies a responsive 5.2% horizontal inset. Keep it, - // while replacing its vertical inset with the overlay clearances used before the - // shape fix. This avoids both duplicated padding and a shortened list viewport. + // Keep Wear Material's responsive horizontal inset while replacing its vertical + // inset with the overlay clearances used before the shape fix. This avoids both + // duplicated padding and a shortened list viewport. contentPadding = scaffoldPadding.withVerticalClearance( layoutDirection = layoutDirection, top = CHAT_HEADER_CONTENT_CLEARANCE, diff --git a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt index fa83e59b..1994f976 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/PeerDebugScreen.kt @@ -46,11 +46,10 @@ fun PeerDebugScreen() { val rssi = mesh?.getPeerRSSI() ?: emptyMap() val identityRevision by WearPeerIdentityState.revision.collectAsState() - ScreenScaffold(scrollState = listState) { scaffoldPadding -> + ScreenScaffold(scrollState = listState) { ScalingLazyColumn( state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = scaffoldPadding + modifier = Modifier.fillMaxSize() ) { item { ListHeader { diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt index e24671ff..738e56cf 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/WearContentPadding.kt @@ -14,18 +14,10 @@ internal fun PaddingValues.withAdditionalPadding( horizontal: Dp = 0.dp, vertical: Dp = 0.dp ): PaddingValues { - val start = when (layoutDirection) { - LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) - LayoutDirection.Rtl -> calculateRightPadding(layoutDirection) - } - val end = when (layoutDirection) { - LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) - LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) - } return PaddingValues( - start = start + horizontal, + start = calculateStartPadding(layoutDirection) + horizontal, top = calculateTopPadding() + vertical, - end = end + horizontal, + end = calculateEndPadding(layoutDirection) + horizontal, bottom = calculateBottomPadding() + vertical ) } @@ -40,13 +32,22 @@ internal fun PaddingValues.withVerticalClearance( top: Dp, bottom: Dp ): PaddingValues { - val start = when (layoutDirection) { + return PaddingValues( + start = calculateStartPadding(layoutDirection), + top = top, + end = calculateEndPadding(layoutDirection), + bottom = bottom + ) +} + +private fun PaddingValues.calculateStartPadding(layoutDirection: LayoutDirection): Dp = + when (layoutDirection) { LayoutDirection.Ltr -> calculateLeftPadding(layoutDirection) LayoutDirection.Rtl -> calculateRightPadding(layoutDirection) } - val end = when (layoutDirection) { + +private fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection): Dp = + when (layoutDirection) { LayoutDirection.Ltr -> calculateRightPadding(layoutDirection) LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection) } - return PaddingValues(start = start, top = top, end = end, bottom = bottom) -} diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt index 477fad77..60191d67 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearContentPaddingTest.kt @@ -37,4 +37,34 @@ class WearContentPaddingTest { assertEquals(30.dp, resolved.calculateTopPadding()) assertEquals(64.dp, resolved.calculateBottomPadding()) } + + @Test + fun `additional padding preserves logical start and end in rtl`() { + val resolved = PaddingValues(start = 10.dp, top = 18.dp, end = 12.dp, bottom = 20.dp) + .withAdditionalPadding( + layoutDirection = LayoutDirection.Rtl, + horizontal = 10.dp, + vertical = 8.dp + ) + + assertEquals(22.dp, resolved.calculateLeftPadding(LayoutDirection.Rtl)) + assertEquals(20.dp, resolved.calculateRightPadding(LayoutDirection.Rtl)) + assertEquals(26.dp, resolved.calculateTopPadding()) + assertEquals(28.dp, resolved.calculateBottomPadding()) + } + + @Test + fun `vertical clearance preserves logical start and end in rtl`() { + val resolved = PaddingValues(start = 10.dp, top = 20.dp, end = 12.dp, bottom = 20.dp) + .withVerticalClearance( + layoutDirection = LayoutDirection.Rtl, + top = 30.dp, + bottom = 64.dp + ) + + assertEquals(12.dp, resolved.calculateLeftPadding(LayoutDirection.Rtl)) + assertEquals(10.dp, resolved.calculateRightPadding(LayoutDirection.Rtl)) + assertEquals(30.dp, resolved.calculateTopPadding()) + assertEquals(64.dp, resolved.calculateBottomPadding()) + } } From 7cf5286bfbc2c2085083f78874d586fa1f5f6c02 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:28:17 +0200 Subject: [PATCH 33/45] Fix slow Wear scroll intent detection --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 60 ++++++++++------ .../bitchat/watch/ui/ChatAutoScrollTest.kt | 72 ++++++++++++++++--- 2 files changed, 104 insertions(+), 28 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 15b18436..f304446e 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -50,6 +50,7 @@ import com.bitchat.watch.ui.theme.ChatVisualTokens import com.bitchat.watch.ui.theme.LocalBitchatPalette import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first +import kotlin.math.sign /** * The shared chat body for global chat and DM threads, following the classic messenger @@ -95,6 +96,7 @@ fun ChatScaffold( val controlsVisible = remember { mutableStateOf(true) } LaunchedEffect(columnState) { var lastPosition = -1 + var scrollIntent = ChatScrollIntentState() snapshotFlow { val first = columnState.layoutInfo.visibleItems.firstOrNull() ChatScrollSnapshot( @@ -103,21 +105,13 @@ fun ChatScaffold( position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0) ) }.collect { snapshot -> - followNewest = updatedFollowNewest( - current = followNewest, + scrollIntent = updatedChatScrollIntent( + current = scrollIntent, snapshot = snapshot, previousPosition = lastPosition ) - if (!snapshot.canScrollForward) { - controlsVisible.value = true - } else if (lastPosition >= 0) { - when { - snapshot.position > lastPosition + CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> - controlsVisible.value = true - snapshot.position < lastPosition - CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> - controlsVisible.value = false - } - } + followNewest = scrollIntent.followsNewest + controlsVisible.value = scrollIntent.controlsVisible lastPosition = snapshot.position } } @@ -173,16 +167,42 @@ internal data class ChatScrollSnapshot( val position: Int ) -internal fun updatedFollowNewest( - current: Boolean, +internal data class ChatScrollIntentState( + val followsNewest: Boolean = true, + val controlsVisible: Boolean = true, + val accumulatedDeltaPx: Int = 0 +) + +internal fun updatedChatScrollIntent( + current: ChatScrollIntentState, snapshot: ChatScrollSnapshot, previousPosition: Int -): Boolean = when { - !snapshot.canScrollForward -> true - snapshot.isScrollInProgress && - previousPosition >= 0 && - snapshot.position < previousPosition - CHAT_SCROLL_DIRECTION_THRESHOLD_PX -> false - else -> current +): ChatScrollIntentState { + if (!snapshot.canScrollForward) return ChatScrollIntentState() + if (!snapshot.isScrollInProgress || previousPosition < 0) return current + + val delta = snapshot.position - previousPosition + val accumulatedDelta = when { + delta == 0 -> current.accumulatedDeltaPx + current.accumulatedDeltaPx == 0 || + current.accumulatedDeltaPx.sign == delta.sign -> + current.accumulatedDeltaPx + delta + else -> delta + } + val movedAway = accumulatedDelta <= -CHAT_SCROLL_DIRECTION_THRESHOLD_PX + val movedTowardNewest = accumulatedDelta >= CHAT_SCROLL_DIRECTION_THRESHOLD_PX + + return current.copy( + followsNewest = current.followsNewest && !movedAway, + controlsVisible = when { + movedAway -> false + movedTowardNewest -> true + else -> current.controlsVisible + }, + // Keep sub-threshold movement across discrete rotary events. Once intent is clear, + // start a fresh accumulator so reversing direction gets the same threshold treatment. + accumulatedDeltaPx = if (movedAway || movedTowardNewest) 0 else accumulatedDelta + ) } internal suspend fun scrollToNewestAfterItemsMeasured( diff --git a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt index ad7c687a..7b2d13b9 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt @@ -12,8 +12,8 @@ class ChatAutoScrollTest { @Test fun `new scroll range from appended message keeps follow intent`() { - val updated = updatedFollowNewest( - current = true, + val updated = updatedChatScrollIntent( + current = ChatScrollIntentState(), snapshot = ChatScrollSnapshot( canScrollForward = true, isScrollInProgress = false, @@ -22,13 +22,13 @@ class ChatAutoScrollTest { previousPosition = 100 ) - assertEquals(true, updated) + assertEquals(ChatScrollIntentState(), updated) } @Test fun `user scroll away disables follow until list reaches newest again`() { - val browsingHistory = updatedFollowNewest( - current = true, + val browsingHistory = updatedChatScrollIntent( + current = ChatScrollIntentState(), snapshot = ChatScrollSnapshot( canScrollForward = true, isScrollInProgress = true, @@ -36,9 +36,10 @@ class ChatAutoScrollTest { ), previousPosition = 100 ) - assertFalse(browsingHistory) + assertFalse(browsingHistory.followsNewest) + assertFalse(browsingHistory.controlsVisible) - val dockedAgain = updatedFollowNewest( + val dockedAgain = updatedChatScrollIntent( current = browsingHistory, snapshot = ChatScrollSnapshot( canScrollForward = false, @@ -47,7 +48,62 @@ class ChatAutoScrollTest { ), previousPosition = 60 ) - assertEquals(true, dockedAgain) + assertEquals(ChatScrollIntentState(), dockedAgain) + } + + @Test + fun `slow scroll away accumulates intent across sub-threshold updates`() { + var state = ChatScrollIntentState() + var previousPosition = 100 + + listOf(94, 88, 82, 76).forEach { position -> + state = updatedChatScrollIntent( + current = state, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = true, + position = position + ), + previousPosition = previousPosition + ) + previousPosition = position + state = updatedChatScrollIntent( + current = state, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = false, + position = position + ), + previousPosition = previousPosition + ) + } + + assertFalse(state.followsNewest) + assertFalse(state.controlsVisible) + assertEquals(0, state.accumulatedDeltaPx) + } + + @Test + fun `slow scroll toward newest reveals controls without restoring follow early`() { + var state = ChatScrollIntentState(followsNewest = false, controlsVisible = false) + var previousPosition = 60 + + listOf(66, 72, 78, 84).forEach { position -> + state = updatedChatScrollIntent( + current = state, + snapshot = ChatScrollSnapshot( + canScrollForward = true, + isScrollInProgress = true, + position = position + ), + previousPosition = previousPosition + ) + previousPosition = position + } + + assertFalse(state.followsNewest) + assertEquals(true, state.controlsVisible) + assertEquals(0, state.accumulatedDeltaPx) } @Test From 1c33df74071937259485afc646fc85b0cc1848f7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 30 Aug 2026 06:16:56 +0000 Subject: [PATCH 37/45] Automated update of relay data - Sun Aug 30 06:16:55 UTC 2026 --- app/src/main/assets/nostr_relays.csv | 769 +++++++++++++-------------- 1 file changed, 379 insertions(+), 390 deletions(-) diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index a503b348..61c0c05c 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,400 +1,389 @@ Relay URL,Latitude,Longitude -myvoiceourstory.org,37.3598,-121.981 -nostr.linky.fit,50.1109,8.68213 -public.obelisk.ar,43.6532,-79.3832 -relay.i9.eti.br,43.6532,-79.3832 -yabu.me,35.6092,139.73 -relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -relay.nostu.be,40.4167,-3.70329 -relay.favillakey.io,-27.4705,153.026 -nostr.mikoshi.de,50.1109,8.68213 -relay.agilesolutionlabs.com,43.6532,-79.3832 -nostr.sovereignservices.xyz,43.6532,-79.3832 -relay.qstr.app,50.1109,8.68213 -articles.layer3.news:443,37.3387,-121.885 -relay.nostrops.com,32.71,-96.6745 -relay.mrmave.work,43.6532,-79.3832 -nostr.islandarea.net:443,35.4669,-97.6473 -nostr.azzamo.net:443,52.2633,21.0283 -ec2.f7z.io,60.1699,24.9384 -relay.veganostr.com,60.1699,24.9384 -nostr.dlcdevkit.com,40.0992,-83.1141 -relay.44billion.net,43.6532,-79.3832 -bendernostur.duckdns.org:8443,50.1109,8.68213 -nostras.app,38.7946,-106.535 -cs-relay.nostrdev.com:443,50.4754,12.3683 -nostr.stakey.net,52.3676,4.90414 -support.flotilla.social,32.9483,-96.7299 -relay.ordoplay.com,50.1109,8.68213 -nostr.stakey.net:443,52.3676,4.90414 -relay.nostrmap.net,60.1699,24.9384 -relayone.geektank.ai,39.0997,-94.5786 -relay.novospes.com,43.6532,-79.3832 -relay.dyne.org,49.0291,8.35705 -nostrrelay.taylorperron.com,45.5029,-73.5723 -nostrbtc.com,43.6532,-79.3832 -relay.lanavault.space,60.1699,24.9384 -relay.bullishbounty.com:443,43.6532,-79.3832 -relay.layer.systems:443,49.0291,8.35695 -vault.iris.to:443,43.6532,-79.3832 -relay.getsafebox.app:443,43.6532,-79.3832 -relay.wellorder.net,45.5201,-122.99 -relay.scuba323.com,40.8218,-74.45 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -temp.iris.to,43.6532,-79.3832 -relay.paulstephenborile.com,49.4543,11.0746 -relay.cyberguy.fyi,52.6907,4.8181 -nostr.wecsats.io:443,43.6532,-79.3832 -bread.nostrsms.com,40.8218,-74.45 -relay.plebeian.market,50.1109,8.68213 -relay.laantungir.net,-19.4692,-42.5315 -nostr.overmind.lol,43.6532,-79.3832 -nip85.nosfabrica.com,39.0997,-94.5786 -relay.conduit.market,38.7946,-106.535 -relay.sigit.io,50.4754,12.3683 -relay.dreamith.to,43.6532,-79.3832 -relay.decentralia.fr,48.122,11.589 -nexus.libernet.app,43.6532,-79.3832 -relay.nmail.li,50.9871,2.12554 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -nostr-relay.nextblockvending.com,47.2343,-119.853 -nostr-relay.corb.net,39.6478,-104.988 -relay.agentry.com,42.8864,-78.8784 -budabit.nostr1.com,40.7057,-74.0136 -relay.cypherflow.ai,48.8575,2.35138 -purplerelay.com,43.6532,-79.3832 -nostr.islandarea.net,35.4669,-97.6473 -relay.edufeed.org,49.4521,11.0767 -relay.bnos.space,43.6532,-79.3832 -relay.kizuna-miki.com,35.694,139.754 -nostr.snowbla.de,50.4754,12.3683 -bitchat.nostr1.com,40.7057,-74.0136 -relay.beamhop.com,43.6532,-79.3832 -relay.wavefunc.live,41.8781,-87.6298 -relayrs.notoshi.win,43.6532,-79.3832 -relay.nostrhub.fr,48.1045,11.6004 -relay01.lnfi.network,35.6764,139.65 -relay.bitos.space,43.6532,-79.3832 -relay.noeudlibre.fr,50.6924,3.20113 -relay.orly.dev,32.7767,-96.797 -relay.paulstephenborile.com:443,49.4543,11.0746 -relay.atsocy.com,43.6532,-79.3832 -relay.keykeeper.world,40.7824,-74.0711 -nostrmxn.lulus.com.mx,59.4016,17.9455 -maxq.descendant.io,43.6532,-79.3832 -nostr.spicyz.io:443,43.6532,-79.3832 -nostr.chaima.info:443,50.1109,8.68213 -relay.opmaat.org,60.1699,24.9384 -nostr.notribe.net,40.8302,-74.1299 -nostr-01.yakihonne.com:443,1.32123,103.695 -offchain.pub,39.1585,-94.5728 -relay.agora.social,50.7383,15.0648 -relay.tdw.lol,41.8781,-87.6298 -nostr.nodesmap.com,59.3327,18.0656 -relay.getvia.xyz,60.1699,24.9384 -public.crostr.com,43.6532,-79.3832 -relay.agorist.space,52.3734,4.89406 -relay.lacrypta.ar,43.6532,-79.3832 -nostr.spaceshell.xyz,43.6532,-79.3832 -relay.nuts.cash,52.3676,4.90414 -relay.otrta.me,50.1109,8.68213 -relay.satmaxt.xyz,43.6532,-79.3832 -relay.earthly.city,34.1749,-118.54 -relay.wisp.talk:443,49.4543,11.0746 -mostro-p2p.tech,50.1109,8.68213 -nostr.thalheim.io,60.1699,24.9384 -relay.s-w.art,43.6532,-79.3832 -nostr.chaima.info,50.1109,8.68213 -articles.layer3.news,37.3387,-121.885 -nostr2.girino.org:443,43.6532,-79.3832 -relay.mostr.pub:443,43.6532,-79.3832 -relay.ditto.pub:443,43.6532,-79.3832 -relay.manneken.brussels,49.4543,11.0746 -relay.kaleidoswap.com,50.8476,4.35717 -nostrelay.circum.space,52.2245,8.826 -relay.beginningend.com,35.2227,-97.4786 -relay-dev.gulugulu.moe,43.6532,-79.3832 -relay.fundstr.me,42.3601,-71.0589 -relay.jmoose.rocks:443,37.4419,-122.143 -relay.nostr.dev.br,48.1046,11.6002 -relay.minibolt.info,43.6532,-79.3832 -nostr.unkn0wn.world,46.8499,9.53287 -vault.iris.to,43.6532,-79.3832 -relay.guggero.org,46.5971,9.59652 -relay.getsafebox.app,43.6532,-79.3832 -relay.bitmacro.cloud,43.6532,-79.3832 -relay.wavlake.com,41.2619,-95.8608 -nostr.hekster.org:443,37.3986,-121.964 -relay.lightning.pub,39.0438,-77.4874 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -soloco.nl,43.6532,-79.3832 -nostr.hifish.org,47.4009,8.57814 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 dm-test-strfry-generic.samt.st,43.6532,-79.3832 -relay.wavefunc.live:443,41.8781,-87.6298 -porchlight.social,43.6532,-79.3832 -relay.zone667.com,60.1699,24.9384 -nostr.data.haus:443,50.4754,12.3683 -relay.nexterz.com,43.6532,-79.3832 -relay.sigit.io:443,50.4754,12.3683 -nostr.girino.org,43.6532,-79.3832 -nostr.liberty.fans,36.8767,-89.5879 -wheat.happytavern.co,43.6532,-79.3832 -relay.0xchat.com,43.6532,-79.3832 -inbox.scuba323.com,40.8218,-74.45 -nostr.vulpem.com,49.4543,11.0746 -relay.damustr.com,43.6532,-79.3832 -no.str.cr:443,8.96171,-83.5246 -relay.vrtmrz.net:443,43.6532,-79.3832 -vm-1734.lnvps.cloud,53.3498,-6.26031 -testnet.samt.st,43.6532,-79.3832 -nostr.rtvslawenia.com:443,49.4543,11.0746 -nostr.sathoarder.com:443,48.5734,7.75211 -relay.cosmicbolt.net,37.3986,-121.964 -relay.staging.plebeian.market,51.5072,-0.127586 -relayone.soundhsa.com,39.0997,-94.5786 -relay.olas.app:443,60.1699,24.9384 -relay.illuminodes.com,43.6532,-79.3832 -relay.chorus.community:443,48.5333,10.7 -relay.ohstr.com:443,43.6532,-79.3832 -fanfares.nostr1.com:443,40.7057,-74.0136 -nostr.planix.org,43.6532,-79.3832 -schnorr.me,43.6532,-79.3832 -relay.mostro.network,40.8302,-74.1299 -nostr-verified.wellorder.net,45.5201,-122.99 -ynostr.yael.at,60.1699,24.9384 -nostr.wild-vibes.ts.net,48.8566,2.35222 -relay.mypathtofire.de,42.8864,-78.8784 -relay.klabo.world,47.674,-122.122 -nostr.tagomago.me,42.3601,-71.0589 -relay.veganostr.com:443,60.1699,24.9384 -nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 -relay.loveisbitcoin.com,43.6532,-79.3832 -bridge.tagomago.me,42.3601,-71.0589 -relay-rpi.edufeed.org:443,49.4521,11.0767 -relay.kilombino.com,43.6532,-79.3832 -relay.sharegap.net,43.6532,-79.3832 -nostr.yutakobayashi.com,43.6532,-79.3832 -relay-na1.metanomalist.com,43.6532,-79.3832 -public.crostr.com:443,43.6532,-79.3832 -nostrride.io,37.3986,-121.964 -nostr.bgbitcoin.club:443,50.4754,12.3683 -nostr.christiansass.de,50.1109,8.68213 -nos.lol:443,50.4754,12.3683 -relay.pyramid.li,47.4093,8.46503 -relay.mappingbitcoin.com,43.6532,-79.3832 -nostr-relay.zimage.com,34.282,-118.439 -offchain.pub:443,39.1585,-94.5728 -nostr.myshosholoza.co.za:443,52.3676,4.90414 -0x-nostr-relay.fly.dev,38.7946,-106.535 -relay1.privkey.io,25.7617,-80.1918 -chat-relay.zap-work.com:443,43.6532,-79.3832 -relay.staging.plebeian.market:443,51.5072,-0.127586 -nostr.robosats.org,64.1476,-21.9392 -relay.endfiat.money:443,59.3327,18.0656 -nostr.agh.ccwu.cc,43.6532,-79.3832 -nostr.overmind.lol:443,43.6532,-79.3832 -nostr.tac.lol,47.4748,-122.273 -relay.ru.ac.th,13.7607,100.627 -nostr-relay.xbytez.io,50.6924,3.20113 -relay.islandbitcoin.com,12.8498,77.6545 -relay5.bitransfer.org,43.6532,-79.3832 -prl.plus,55.7628,37.5983 -chat-relay.zap-work.com,43.6532,-79.3832 -relay.satlantis.io,39.0438,-77.4874 -nostr.bitcoiner.social:443,40.7608,-111.891 -nostr.ac,38.958,-77.3592 -relay.solomonstr.com,43.6532,-79.3832 -strfry.apps3.slidestr.net,40.4167,-3.70329 -relay.nostr.com,50.1109,8.68213 -reraw.pbla2fish.cc,43.6532,-79.3832 -nostr.bond,50.1109,8.68213 -relay.wavlake.com:443,41.2619,-95.8608 -nos.lol,50.4754,12.3683 -nostr.debate.report,50.1109,8.68213 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -relay.wisp.talk,49.4543,11.0746 -nostr.tabordalab.com,60.1699,24.9384 -relay.trotters.cc,43.6532,-79.3832 -nostr.4rs.nl,49.0291,8.35696 -relay.hivetalk.org,40.8302,-74.1299 -spamspamspamspam.rest,43.6532,-79.3832 -nostr.janx.com,43.6532,-79.3832 -relay.mostr.pub,43.6532,-79.3832 -relay.lightning.pub:443,39.0438,-77.4874 -relay.samt.st,40.8302,-74.1299 -bruh.samt.st,43.6532,-79.3832 -relay.orangesync.tech,40.7128,-74.006 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -relay.notoshi.win,13.3396,100.93 -relay.nostreon.com,60.1699,24.9384 -nostr.purpura.cloud,43.6532,-79.3832 -basspistol.org,49.0291,8.35696 -relay.nostrfeed.com,60.1699,24.9384 -nostr.dlcdevkit.com:443,40.0992,-83.1141 -nostr.21crypto.ch,47.5356,8.73209 -nostr.pbfs.io,50.4754,12.3683 -social.amanah.eblessing.co,48.1046,11.6002 -relay.cosmicbolt.net:443,37.3986,-121.964 -21milionidinostr.duckdns.org,41.8967,12.4822 -fanfares.nostr1.com,40.7057,-74.0136 -nostr.na.social,43.6532,-79.3832 -nostr.data.haus,50.4754,12.3683 -strfry.shock.network:443,39.0438,-77.4874 -nostr.pbfs.io:443,50.4754,12.3683 -relay.shadowbip.com,50.1109,8.68213 -relay.ltgnet.work:8443,51.0511,-114.075 -relay02.lnfi.network,35.6764,139.65 -nostr.myshosholoza.co.za,52.3676,4.90414 -relay.satsmarkt.club,52.6907,4.8181 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -auth.nostr1.com,40.7057,-74.0136 -dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -bitcoinostr.duckdns.org,38.9504,-0.14007 -relayrs.notoshi.win:443,43.6532,-79.3832 -nostr.whitenode45.ddns.net,40.55,-74.4758 -relay.mccormick.cx:443,52.3563,4.95714 -nostr.2b9t.xyz:443,34.0549,-118.243 -relay.internationalright-wing.org,-22.4692,-48.9875 -nostr.2b9t.xyz,34.0549,-118.243 -relay.mitchelltribe.com,39.0438,-77.4874 -relay.nostriot.com,41.5695,-83.9786 -infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 -relay.sector01.com,41.4513,-81.7021 -relay.nostx.io,43.6532,-79.3832 -relay.fiatdenier.com,43.7787,-79.5393 -relayone.soundhsa.com:443,39.0997,-94.5786 -strfry.bonsai.com:443,39.0438,-77.4874 -nostr.snowbla.de:443,50.4754,12.3683 -relay.btcforplebs.com,43.6532,-79.3832 -dev.relay.stream,43.6532,-79.3832 -nostr-pub.wellorder.net,45.5201,-122.99 -nostr.twinkle.lol,51.902,7.6657 -nostr.carroarmato0.be,50.914,3.21378 -bcast.girino.org,43.6532,-79.3832 -treuzkas.branruz.com,48.8575,2.35138 -dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 -relay.endfiat.money,59.3327,18.0656 -relay.jmoose.rocks,37.4419,-122.143 -relay.degmods.com,50.4754,12.3683 -relay.ditto.pub,43.6532,-79.3832 nostrelay.circum.space:443,52.2245,8.826 -relay.pocketnostr.com,40.8054,-74.0241 -nostr.wecsats.io,43.6532,-79.3832 -node.kommonzenze.de,49.4521,11.0767 +relay.staging.plebeian.market,51.5072,-0.127586 +nostr.chaima.info:443,50.1109,8.68213 +relay.vrtmrz.net:443,43.6532,-79.3832 +relay.gulugulu.moe:443,43.6532,-79.3832 +relay.dreamith.to,43.6532,-79.3832 +nostrride.io,37.3986,-121.964 +nostr.n7ekb.net,47.4941,-122.294 +relay.npubhaus.com,43.6532,-79.3832 +relay1.privkey.io,25.7617,-80.1918 +nostr-relay.xbytez.io:443,50.6924,3.20113 +relay.bullishbounty.com,43.6532,-79.3832 +nostr.islandarea.net,35.4669,-97.6473 +nostrrelay.taylorperron.com,45.5029,-73.5723 +relay.sharegap.net,43.6532,-79.3832 +relayone.geektank.ai,39.0997,-94.5786 +nexus.libernet.app,43.6532,-79.3832 +fanfares.nostr1.com:443,40.7057,-74.0136 +relay.satlantis.io,39.0438,-77.4874 +relay.hivetalk.org,40.8302,-74.1299 +nos.lol,50.4754,12.3683 +staging.yabu.me,35.6092,139.73 +relay-us.zombi.cloudrodion.com,40.7862,-74.0743 +relay.internationalright-wing.org,-22.4692,-48.9875 +relay.nostrcheck.me,43.6532,-79.3832 +denimroad.feeds.relay.tools,38.6327,-90.1961 +relay.nostr-check.me,43.6532,-79.3832 +ribo.us.nostria.app:443,43.6532,-79.3832 +relay.lightning.pub,39.0438,-77.4874 +myvoiceourstory.org,37.3598,-121.981 +relay.jmoose.rocks:443,37.4419,-122.143 +nostr-pr02.redscrypt.org,52.3676,4.90414 +nostr.purpura.cloud,43.6532,-79.3832 +relay.sigit.io:443,50.4754,12.3683 +relay.nostrops.com,32.71,-96.6745 +relay.jmoose.rocks,37.4419,-122.143 +nostr.spaceshell.xyz,43.6532,-79.3832 +nostr-pub.wellorder.net,45.5201,-122.99 +relay.opmaat.org,60.1699,24.9384 +nostr.myshosholoza.co.za,52.3676,4.90414 +offchain.bostr.online,43.6532,-79.3832 +nostr-relay.nilpote.com,43.6532,-79.3832 +relay.nostu.be,40.4167,-3.70329 +nostr.stakey.net,52.3676,4.90414 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +conduitl2.fly.dev,38.7946,-106.535 +vault.iris.to:443,43.6532,-79.3832 +relay.minibolt.info,43.6532,-79.3832 +articles.layer3.news:443,37.3387,-121.885 +nostr.overmind.lol,43.6532,-79.3832 +nostr.21crypto.ch,47.5356,8.73209 +nostr.tabordalab.com,60.1699,24.9384 +nostr.spicyz.io,43.6532,-79.3832 nostr.sathoarder.com,48.5734,7.75211 +relay.chorus.community,48.5333,10.7 +dev.relay.stream,43.6532,-79.3832 +nostr-relay.corb.net:443,39.6478,-104.988 +relay.lightning.pub:443,39.0438,-77.4874 +nostr.tbxnetworx.de,47.8226,10.0064 +nostr.azzamo.net,52.2633,21.0283 +relay.loveisbitcoin.com,43.6532,-79.3832 +strfry.ymir.cloud,43.6532,-79.3832 chorus.mikedilger.com:444,-36.8906,174.794 -cache.trustr.ing,43.6548,-79.3885 -bitcoiner.social,40.7608,-111.891 -relay.tdw.gg,34.0549,-118.243 -relay.edufeed.org:443,49.4521,11.0767 -relay.snotr.nl:49999,51.9758,4.31389 -nostr.bgbitcoin.club,50.4754,12.3683 -relay.bowlafterbowl.com,32.9483,-96.7299 -relay-rpi.edufeed.org,49.4521,11.0767 -x.kojira.io:443,43.6532,-79.3832 -nostr.self-determined.de,51.4379,14.2355 -schnorr.me:443,43.6532,-79.3832 +relay.agilesolutionlabs.com,43.6532,-79.3832 +relay.gulugulu.moe,43.6532,-79.3832 +nostr.tac.lol,47.4748,-122.273 +relay.mypathtofire.de,42.8864,-78.8784 +nostrelay.circum.space,52.2245,8.826 +bcast.girino.org,43.6532,-79.3832 +relay.ordoplay.com,50.1109,8.68213 +nostr.chaima.info,50.1109,8.68213 +offchain.pub:443,39.1585,-94.5728 +relay.wisp.talk,49.4543,11.0746 +strfry.bonsai.com,39.0438,-77.4874 +nostr.2b9t.xyz:443,34.0549,-118.243 +relay.nostx.io,43.6532,-79.3832 +bucket.coracle.social,37.7775,-122.397 +relay.wavlake.com:443,41.2619,-95.8608 +nostr.carroarmato0.be:443,50.914,3.21378 +treuzkas.branruz.com,48.8575,2.35138 nostr.computingcache.com,45.5341,-122.956 cs-relay.nostrdev.com,50.4754,12.3683 -relay2.veganostr.com,60.1699,24.9384 -relay.phostrich.com,40.55,-74.4758 -relay.nostrian-conquest.com,41.223,-111.974 -relay.flashapp.me,43.6548,-79.3885 -relay.yoinekodo.jp,43.6532,-79.3832 -relay-arg.zombi.cloudrodion.com,1.35208,103.82 -freelay.sovbit.host,60.1699,24.9384 -nostr.n7ekb.net,47.4941,-122.294 -purplerelay.com:443,43.6532,-79.3832 -nostr.tbxnetworx.de,47.7228,9.96456 -relay.bnos.space:443,43.6532,-79.3832 -relay.satsapp.me,50.4754,12.3683 -nostr-01.uid.ovh,50.9871,2.12554 -tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 -nostr.mom,50.4754,12.3683 -no.str.cr,8.96171,-83.5246 -nostr.spicyz.io,43.6532,-79.3832 -memlay.v0l.io,53.3498,-6.26031 -bucket.coracle.social,37.7775,-122.397 -nostr.azzamo.net,52.2633,21.0283 relay.lanacoin-eternity.com,40.8302,-74.1299 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -conduitl2.fly.dev,38.7946,-106.535 -relay.wasabiwallet.io,43.6532,-79.3832 -portal-relay.pareto.space,49.0291,8.35696 -strfry.ymir.cloud,43.6532,-79.3832 -nostr.hoppe-relay.it.com,42.8864,-78.8784 -nostr.red5d.dev,43.6532,-79.3832 -strfry.shock.network,39.0438,-77.4874 -relay.chorus.community,48.5333,10.7 -relay.mitchelltribe.com:443,39.0438,-77.4874 -relay.olas.app,60.1699,24.9384 -testnet-relay.samt.st:443,40.8302,-74.1299 -relay.dwadziesciajeden.pl,52.2297,21.0122 -relay.gulugulu.moe,43.6532,-79.3832 -relay.underorion.se,50.1109,8.68213 -relay.mccormick.cx,52.3563,4.95714 -top.testrelay.top,43.6532,-79.3832 -nostr-02.uid.ovh,50.9871,2.12554 -nostr.relay.hedwig.sh,60.1699,24.9384 -nostr-01.yakihonne.com,1.32123,103.695 -relay.nostr.net,43.6532,-79.3832 -nostr-relay.cbrx.io,43.6532,-79.3832 -nrs-01.darkcloudarcade.com,39.0997,-94.5786 -relay.trotters.cc:443,43.6532,-79.3832 -strfry.bonsai.com,39.0438,-77.4874 -relay.snort.social,53.3498,-6.26031 -nostr.hekster.org,37.3986,-121.964 -relay.mleku.dev,32.7767,-96.797 -nostr-relay.xbytez.io:443,50.6924,3.20113 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -nostr.davenov.com,50.1109,8.68213 -relay.chatbett.de,40.7128,-74.006 -relayone.geektank.ai:443,39.0997,-94.5786 -insta-relay.apps3.slidestr.net,40.4167,-3.70329 -nostr.mom:443,50.4754,12.3683 -nostr-relay.corb.net:443,39.6478,-104.988 -relay.gulugulu.moe:443,43.6532,-79.3832 -nostr.thebiglake.org,32.71,-96.6745 -relay.nearhood.co.uk,51.5134,-0.0890675 -relay1.nostrchat.io,60.1699,24.9384 -rele.speyhard.fi,50.1109,8.68213 -relay.bornheimer.app,50.1109,8.68213 -nostr.fullstackcash.net,45.5201,-122.99 -relay2.fiatdenier.com,50.1013,8.62643 -relay.solife.me,43.6532,-79.3832 -syb.lol,34.0549,-118.243 -dev-relay.nostreon.com,60.1699,24.9384 -relay.agorist.space:443,52.3734,4.89406 -nostr.easycryptosend.it,43.6532,-79.3832 -x.kojira.io,43.6532,-79.3832 -adre.su,59.9311,30.3609 -offchain.bostr.online,43.6532,-79.3832 -relay.libernet.app,43.6532,-79.3832 -nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 nostr.rtvslawenia.com,49.4543,11.0746 -relay.layer.systems,49.0291,8.35695 -relay.arx-ccn.com,50.4754,12.3683 -staging.yabu.me,35.6092,139.73 -relay.vrtmrz.net,43.6532,-79.3832 -nostr.bitcoiner.social,40.7608,-111.891 -nostr.88mph.life,52.1941,-2.21905 -relay.pprgb.app,52.52,13.405 -relay.aarpia.com,37.3986,-121.964 -nostrcity-club.fly.dev,38.7946,-106.535 -buzz.ac2n-share.kozow.com,45.764,4.83566 -relay.fckstate.net,59.3293,18.0686 -nostr.thalheim.io:443,60.1699,24.9384 -nostr.carroarmato0.be:443,50.914,3.21378 -nostrcity-club.fly.dev:443,38.7946,-106.535 -relay.routstr.com,59.4016,17.9455 -nostr-dev.wellorder.net,45.5201,-122.99 -relay.directsponsor.net,42.8864,-78.8784 -relay.mwaters.net,50.9871,2.12554 -relay.littlebitstudios.com,43.6532,-79.3832 -relay.ohstr.com,43.6532,-79.3832 -testnet-relay.samt.st,40.8302,-74.1299 -relay.bullishbounty.com,43.6532,-79.3832 -relay.nostrmap.net:443,60.1699,24.9384 +nip85.nosfabrica.com,39.0997,-94.5786 +relay.wisp.talk:443,49.4543,11.0746 +nostr.stakey.net:443,52.3676,4.90414 relay.piazza.today,48.122,11.589 +relay.conduit.market,38.7946,-106.535 +relay.nostrmap.net,60.1699,24.9384 +maxq.descendant.io,43.6532,-79.3832 +nostr.hekster.org,37.3986,-121.964 +nostr.christiansass.de,49.7423,8.76687 +tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +relay.scuba323.com,40.8218,-74.45 +schnorr.me,43.6532,-79.3832 +relay.cypherflow.ai,48.8575,2.35138 +nostr.snowbla.de:443,50.4754,12.3683 +nostr.whitenode45.ddns.net,40.55,-74.4758 +adre.su,59.9311,30.3609 +relay.getsafebox.app,43.6532,-79.3832 +auth.nostr1.com,40.7057,-74.0136 +relay2.veganostr.com,60.1699,24.9384 +relay.i9.eti.br,43.6532,-79.3832 +relay.satsmarkt.club,52.6907,4.8181 +nostr.bitcoiner.social:443,47.6743,-117.112 +cache.trustr.ing,43.6548,-79.3885 +relay.mitchelltribe.com,39.0438,-77.4874 +21milionidinostr.duckdns.org,41.8967,12.4822 +public.crostr.com:443,43.6532,-79.3832 +relay.otrta.me,50.1109,8.68213 +relay.bornheimer.app,50.1109,8.68213 +relay.sigit.io,50.4754,12.3683 +nostr-relay.xbytez.io,50.6924,3.20113 +relayone.soundhsa.com,39.0997,-94.5786 +vm-1734.lnvps.cloud,53.3498,-6.26031 +nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 +nostr.twinkle.lol,51.902,7.6657 +nostr.data.haus,50.4754,12.3683 +nostr.mas-family.eu,60.3478,15.7505 +relay.nostrhub.fr,48.1045,11.6004 +nostr.openhoofd.nl,51.5717,3.70417 +relay.nostreon.com,60.1699,24.9384 +relay.tdw.lol,41.8781,-87.6298 +top.testrelay.top,43.6532,-79.3832 +syb.lol,34.0549,-118.243 +relay.primal.net,43.6532,-79.3832 +n.musicroadmap.com,35.694,139.754 +ribo.eu.nostria.app,43.6532,-79.3832 +nostr.red5d.dev,43.6532,-79.3832 +nostr.ltd,19.076,72.8777 +relay.olas.app,60.1699,24.9384 +chat-relay.zap-work.com,43.6532,-79.3832 +relay.islandbitcoin.com,12.8498,77.6545 +relay.chatbett.de,40.7128,-74.006 +relay.novospes.com,43.6532,-79.3832 +nostr.bond,50.1109,8.68213 +inbox.scuba323.com,40.8218,-74.45 +0x-nostr-relay.fly.dev,38.7946,-106.535 +relay.sincensura.org,43.6532,-79.3832 +nostr-02.uid.ovh,50.9871,2.12554 +relay.solomonstr.com,43.6532,-79.3832 +purplerelay.com,43.6532,-79.3832 +nostr.emanuelemiani.it,45.778,8.79414 +relay.damustr.com,43.6532,-79.3832 +fanfares.nostr1.com,40.7057,-74.0136 +relay.plebeian.market:443,50.1109,8.68213 +buzz.ac2n-share.kozow.com,45.764,4.83566 +nostr-01.yakihonne.com:443,1.32123,103.695 +relay.snotr.nl:49999,51.9758,4.31389 +relay.ltgnet.work:8443,51.0511,-114.075 +nostras.app,38.7946,-106.535 +purplerelay.com:443,43.6532,-79.3832 +relay.bnos.space,43.6532,-79.3832 +porchlight.social,43.6532,-79.3832 +relay.mostr.pub:443,43.6532,-79.3832 +relay.layer.systems,49.0291,8.35695 +nostrcity-club.fly.dev,38.7946,-106.535 +relay.guggero.org,46.5971,9.59652 +relay.layer.systems:443,49.0291,8.35695 +relay.edufeed.org,49.4521,11.0767 +nostr.mom:443,50.4754,12.3683 +relay.mccormick.cx,52.3563,4.95714 +public.crostr.com,43.6532,-79.3832 +mostro-p2p.tech,50.1109,8.68213 +strfry.shock.network:443,39.0438,-77.4874 +relay.agentry.com,42.8864,-78.8784 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +relay.nostrian-conquest.com,41.223,-111.974 +relay.fountain.fm,43.6532,-79.3832 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +rele.speyhard.fi,50.1109,8.68213 +reraw.pbla2fish.cc,43.6532,-79.3832 +relay.pyramid.li,47.4093,8.46503 +relay.trotters.cc,43.6532,-79.3832 +relay.ru.ac.th,13.7607,100.627 +relay.orly.dev,32.7767,-96.797 +nostr.yutakobayashi.com,43.6532,-79.3832 +nostr-01.yakihonne.com,1.32123,103.695 +relay.lanavault.space,60.1699,24.9384 +chat-relay.zap-work.com:443,43.6532,-79.3832 +strfry.shock.network,39.0438,-77.4874 +wheat.happytavern.co,43.6532,-79.3832 +relay.klabo.world,47.674,-122.122 +relay.littlebitstudios.com,43.6532,-79.3832 +nostr-relay.corb.net,39.6478,-104.988 +relay.flashapp.me,43.6548,-79.3885 +espelho.girino.org,43.6532,-79.3832 +budabit.nostr1.com,40.7057,-74.0136 +nostr.vulpem.com,49.4543,11.0746 +relay.nostrfeed.com,60.1699,24.9384 +basspistol.org,49.0291,8.35696 +relay-rpi.edufeed.org,49.4521,11.0767 +relay.mitchelltribe.com:443,39.0438,-77.4874 +spamspamspamspam.rest,43.6532,-79.3832 +relayrs.notoshi.win:443,43.6532,-79.3832 +relay.endfiat.money,59.3327,18.0656 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 +nostr.tagomago.me,42.3601,-71.0589 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +nostr.rtvslawenia.com:443,49.4543,11.0746 +no.str.cr:443,8.96171,-83.5246 +relay.ditto.pub,43.6532,-79.3832 +relay.shadowbip.com,54.352,18.6466 +nostr.liberty.fans,36.8767,-89.5879 +prl.plus,55.7628,37.5983 +relay.agorist.space,52.3734,4.89406 +articles.layer3.news,37.3387,-121.885 +nostr.thalheim.io:443,60.1699,24.9384 +relay.mccormick.cx:443,52.3563,4.95714 +support.flotilla.social,32.9483,-96.7299 +nostr.mikoshi.de,50.1109,8.68213 +nostrmxn.lulus.com.mx,59.4016,17.9455 +bridge.tagomago.me,42.3601,-71.0589 +relayrs.notoshi.win,43.6532,-79.3832 +nostr.88mph.life,52.1941,-2.21905 +nostr.fullstackcash.net,45.5201,-122.99 +relay5.bitransfer.org,43.6532,-79.3832 +relay.0xchat.com,43.6532,-79.3832 +relay.nostriot.com,41.5695,-83.9786 +relay.wellorder.net,45.5201,-122.99 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +nostr.relay.hedwig.sh,60.1699,24.9384 +relay.openspecs.uid.ovh,50.9871,2.12554 +relay.keykeeper.world,40.7824,-74.0711 +nostr.wecsats.io:443,43.6532,-79.3832 +relay.wavlake.com,41.2619,-95.8608 +relay.lacrypta.ar,43.6532,-79.3832 +nos.lol:443,50.4754,12.3683 +dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 +relay.nmail.li,50.9871,2.12554 +relay.mostro.network,40.8302,-74.1299 +bitcoinostr.duckdns.org,41.1976,1.11167 +relay.s-w.art,43.6532,-79.3832 +relay.ohstr.com:443,43.6532,-79.3832 +nostr-dev.wellorder.net,45.5201,-122.99 +nostr.notribe.net,40.8302,-74.1299 +relay.mleku.dev,32.7767,-96.797 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +nostr2.girino.org:443,43.6532,-79.3832 +relay.cosmicbolt.net:443,37.3986,-121.964 +ribo.us.nostria.app,43.6532,-79.3832 +nostr.janx.com,43.6532,-79.3832 +familiamartins.net.br,-22.8833,-43.1036 +relay.yoinekodo.jp,43.6532,-79.3832 +nostr.4rs.nl,49.0291,8.35696 +relay.routstr.com,59.4016,17.9455 +relay.bitmacro.cloud,43.6532,-79.3832 +relay.mappingbitcoin.com,43.6532,-79.3832 +nostr.dlcdevkit.com:443,40.0992,-83.1141 +relay-testnet.k8s.layer3.news,37.3387,-121.885 +nostr.overmind.lol:443,43.6532,-79.3832 +relayone.geektank.ai:443,39.0997,-94.5786 +relay.staging.plebeian.market:443,51.5072,-0.127586 +relay.kilombino.com,43.6532,-79.3832 +relay.endfiat.money:443,59.3327,18.0656 +social.amanah.eblessing.co,48.1046,11.6002 +nostr.2b9t.xyz,34.0549,-118.243 +relay.sector01.com,41.4513,-81.7021 +relay-dev.gulugulu.moe:443,43.6532,-79.3832 +relay.nostr.dev.br,48.1046,11.6002 +x.kojira.io,43.6532,-79.3832 +node.kommonzenze.de,49.4521,11.0767 +relay.earthly.city,34.1749,-118.54 +buzz.cashu.space,50.1109,8.68213 +relay.kizuna-miki.com,35.694,139.754 +bitchat.nostr1.com,40.7057,-74.0136 +bruh.samt.st,43.6532,-79.3832 +relay2.fiatdenier.com,50.1013,8.62643 +nostr.oxtr.dev,50.4754,12.3683 +relay.pocketstr.com,40.8302,-74.1299 +nostr.thalheim.io,60.1699,24.9384 +relay.qstr.app,50.1109,8.68213 +ynostr.yael.at,60.1699,24.9384 +relay.zone667.com,60.1699,24.9384 +relay.underorion.se,50.1109,8.68213 +relay.beamhop.com,43.6532,-79.3832 +relay.paulstephenborile.com,49.4543,11.0746 +nostr.easycryptosend.it,43.6532,-79.3832 +nostr-verified.wellorder.net,45.5201,-122.99 +nostr.hekster.org:443,37.3986,-121.964 +relay.samt.st,40.8302,-74.1299 +nostr.islandarea.net:443,35.4669,-97.6473 +nostr-01.uid.ovh,50.9871,2.12554 +relay-dev.gulugulu.moe,43.6532,-79.3832 +relay.44billion.net,43.6532,-79.3832 +dev-relay.nostreon.com,60.1699,24.9384 +nostr.pbfs.io,50.4754,12.3683 +nrs-01.darkcloudarcade.com,39.0997,-94.5786 +relay01.lnfi.network,35.6764,139.65 +relay.mostr.pub,43.6532,-79.3832 +strfry.apps3.slidestr.net,40.4167,-3.70329 +nostr.linky.fit,50.1109,8.68213 +nostr.davenov.com,50.1109,8.68213 +relay.noeudlibre.fr,50.6924,3.20113 +hasky.chat,40.7862,-74.0743 +relay.artio.inf.unibe.ch,46.9501,7.43678 +relay.manneken.brussels,49.4543,11.0746 +relay.veganostr.com,60.1699,24.9384 +relay.nexterz.com,43.6532,-79.3832 +nostr.snowbla.de,50.4754,12.3683 +nostr.unkn0wn.world,46.8499,9.53287 +nostr.wild-vibes.ts.net,48.8566,2.35222 +relay.bitos.space,43.6532,-79.3832 +relay-na1.metanomalist.com,43.6532,-79.3832 +relay.favillakey.io,-27.4705,153.026 +relay.nearhood.co.uk,51.5134,-0.0890675 +relay.illuminodes.com,43.6532,-79.3832 +nostr.dlcdevkit.com,40.0992,-83.1141 +relay.nostr.net,43.6532,-79.3832 +relay.trotters.cc:443,43.6532,-79.3832 +cyberspace.nostr1.com,40.7057,-74.0136 +nostr-relay.nextblockvending.com,47.2343,-119.853 +relay.btcforplebs.com,43.6532,-79.3832 +nostr.azzamo.net:443,52.2633,21.0283 +relay.tdw.gg,34.0549,-118.243 +relay.veganostr.com:443,60.1699,24.9384 +wisp.djorivaltech.com,43.6532,-79.3832 +relay.cosmicbolt.net,37.3986,-121.964 +nostr.debate.report,50.1109,8.68213 +nostr.na.social,43.6532,-79.3832 +relay.kaleidoswap.com,50.8476,4.35717 +nostr.sathoarder.com:443,48.5734,7.75211 +relay.getsafebox.app:443,43.6532,-79.3832 +relay.aarpia.com,37.3986,-121.964 +relay.satsapp.me,50.4754,12.3683 +relay.decentralia.fr,48.122,11.589 +relay.beginningend.com,35.2227,-97.4786 +relay.paulstephenborile.com:443,49.4543,11.0746 +relay.satmaxt.xyz,43.6532,-79.3832 +nostr.oxtr.dev:443,50.4754,12.3683 +vault.iris.to,43.6532,-79.3832 +relay.ditto.pub:443,43.6532,-79.3832 +relay.atsocy.com,43.6532,-79.3832 +relay.vrtmrz.net,43.6532,-79.3832 +soloco.nl,43.6532,-79.3832 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +relayone.soundhsa.com:443,39.0997,-94.5786 +nostr.wawasoft.net,43.6532,-79.3832 +relay.solife.me,43.6532,-79.3832 +nostr.mom,50.4754,12.3683 +relay.getvia.xyz,60.1699,24.9384 +freelay.sovbit.host,60.1699,24.9384 +relay-rpi.edufeed.org:443,49.4521,11.0767 +relay.bullishbounty.com:443,43.6532,-79.3832 +schnorr.me:443,43.6532,-79.3832 +temp.iris.to,43.6532,-79.3832 +nostr-relay.cbrx.io,43.6532,-79.3832 +strfry.bonsai.com:443,39.0438,-77.4874 +nostr.hoppe-relay.it.com,42.8864,-78.8784 +dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 +relay.nostriches.club,43.6532,-79.3832 +relay.dyne.org,49.0291,8.35705 +yabu.me,35.6092,139.73 +relay.chorus.community:443,48.5333,10.7 +cs-relay.nostrdev.com:443,50.4754,12.3683 +relay.mwaters.net,50.9871,2.12554 +no.str.cr,8.96171,-83.5246 +nostr.hifish.org,47.4009,8.57814 +relay.directsponsor.net,42.8864,-78.8784 +public.obelisk.ar,43.6532,-79.3832 +nostr.carroarmato0.be,50.914,3.21378 +nostr.sovereignservices.xyz,43.6532,-79.3832 +nostr.pbfs.io:443,50.4754,12.3683 +relay.degmods.com,50.4754,12.3683 +relay.plebeian.market,50.1109,8.68213 +offchain.pub,39.1585,-94.5728 +nostr.thebiglake.org,32.71,-96.6745 +bendernostur.duckdns.org:8443,50.1109,8.68213 +nostr-relay.zimage.com,34.282,-118.439 +nostr.ac,38.958,-77.3592 +relay.nostr.com,50.1109,8.68213 +bitcoiner.social,47.6743,-117.112 +nostr.wecsats.io,43.6532,-79.3832 +relay.pprgb.app,52.52,13.405 +nostr.girino.org,43.6532,-79.3832 +nostr.bitcoiner.social,47.6743,-117.112 +nostr.planix.org,43.6532,-79.3832 +nostr.data.haus:443,50.4754,12.3683 +relay.nostrmap.net:443,60.1699,24.9384 +relay.arx-ccn.com,50.4754,12.3683 +relay.agorist.space:443,52.3734,4.89406 +relay.laantungir.net,-19.4692,-42.5315 +relay.edufeed.org:443,49.4521,11.0767 +nostrcity-club.fly.dev:443,38.7946,-106.535 +nostr.nodesmap.com,59.3327,18.0656 From 06cdb0f2f6b87eb66ddaad475ad0f7c45da40a4e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:40:15 +0300 Subject: [PATCH 38/45] Bump Android version to 2.0.2 --- 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 26042d6e..ef4cbe53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,8 +30,8 @@ android { applicationId = "com.bitchat.droid" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 38 - versionName = "2.0.1" + versionCode = 39 + versionName = "2.0.2" buildConfigField( "String", "GITHUB_RELEASE_CERT_SHA256", From afad3c551350141381ea7999c6fbbe730a95b28f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:42:43 +0300 Subject: [PATCH 39/45] Fix Wear layouts on small round displays --- .../java/com/bitchat/watch/MainActivity.kt | 179 +++++++-------- .../com/bitchat/watch/ui/ChatActionBar.kt | 210 ++++++++++-------- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 2 +- .../java/com/bitchat/watch/ui/ChatScreen.kt | 79 ++++--- .../java/com/bitchat/watch/ui/DmScreen.kt | 75 +++---- .../bitchat/watch/ui/NicknameSetupScreen.kt | 140 ++++++------ .../com/bitchat/watch/ui/TextInputScreen.kt | 4 +- .../com/bitchat/watch/ui/UserDetailScreen.kt | 15 +- .../watch/ui/VerificationCodeScreen.kt | 16 +- .../com/bitchat/watch/ui/WearChatHeader.kt | 71 ++++++ .../bitchat/watch/ui/WearDisplayGeometry.kt | 16 ++ .../com/bitchat/watch/ui/WearFormScreen.kt | 30 +++ .../com/bitchat/watch/ui/media/MediaItems.kt | 14 +- .../com/bitchat/watch/ui/theme/Typography.kt | 6 +- .../watch/ui/WearDisplayGeometryTest.kt | 47 ++++ .../watch/ui/WearPeerIdentityStateTest.kt | 4 +- 16 files changed, 556 insertions(+), 352 deletions(-) create mode 100644 wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt create mode 100644 wear/src/main/java/com/bitchat/watch/ui/WearDisplayGeometry.kt create mode 100644 wear/src/main/java/com/bitchat/watch/ui/WearFormScreen.kt create mode 100644 wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt diff --git a/wear/src/main/java/com/bitchat/watch/MainActivity.kt b/wear/src/main/java/com/bitchat/watch/MainActivity.kt index fd03e9f3..dc07a949 100644 --- a/wear/src/main/java/com/bitchat/watch/MainActivity.kt +++ b/wear/src/main/java/com/bitchat/watch/MainActivity.kt @@ -18,9 +18,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -30,7 +27,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -39,7 +35,7 @@ import androidx.core.content.ContextCompat import androidx.wear.compose.material3.Button import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Text -import androidx.wear.compose.material3.TextButton +import androidx.wear.compose.material3.OutlinedButton import com.bitchat.watch.mesh.WearMeshService import com.bitchat.watch.notification.WearNotificationCoordinator import com.bitchat.watch.service.WearMeshForegroundService @@ -50,6 +46,7 @@ import com.bitchat.watch.ui.PeopleScreen import com.bitchat.watch.ui.UserDetailScreen import com.bitchat.watch.ui.VerificationCodeScreen import com.bitchat.watch.ui.WearChatState +import com.bitchat.watch.ui.WearFormScreen import com.bitchat.watch.ui.sendPrivateMessage import com.bitchat.watch.ui.sendPublicMessage import com.bitchat.watch.ui.theme.BitchatWearTheme @@ -460,104 +457,110 @@ internal fun WearNavHost( @Composable fun NotificationPermissionScreen(onResult: (Boolean) -> Unit, onSkip: () -> Unit) { - val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> onResult(granted) } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Message alerts", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = "Alerts for encrypted direct messages", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 6.dp, bottom = 10.dp) - ) - Button( - onClick = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - launcher.launch(Manifest.permission.POST_NOTIFICATIONS) - } else { - onResult(true) - } - } - ) { - Text("Enable") + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + onResult(granted) } - TextButton(onClick = onSkip) { - Text("Not now") + + WearFormScreen { + item { + Text( + text = "Message alerts", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + ) + } + item { + Text( + text = "Alerts for encrypted direct messages", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 6.dp, bottom = 10.dp), + ) + } + item { + Button( + onClick = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + onResult(true) + } + } + ) { + Text("Enable") + } + } + item { + OutlinedButton(onClick = onSkip) { + Text("Not now") + } } } } @Composable fun PermissionRequestScreen(onGranted: () -> Unit) { - val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestMultiplePermissions() - ) { onGranted() } + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + onGranted() + } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "bitchat", - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = "Needs Bluetooth to mesh with nearby devices", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 6.dp, bottom = 12.dp) - ) - Button(onClick = { - launcher.launch(MainActivity.requiredPermissions().toTypedArray()) - }) { - Text("Grant access") + WearFormScreen { + item { + Text( + text = "bitchat", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + ) + } + item { + Text( + text = "Needs Bluetooth to mesh with nearby devices", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 6.dp, bottom = 12.dp), + ) + } + item { + Button( + onClick = { + launcher.launch(MainActivity.requiredPermissions().toTypedArray()) + } + ) { + Text("Grant access", textAlign = TextAlign.Center) + } } } } @Composable fun BluetoothEnableScreen(onEnabled: () -> Unit) { - val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { onEnabled() } + val launcher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + onEnabled() + } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Bluetooth is off", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurface - ) - Button( - onClick = { launcher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) }, - modifier = Modifier.padding(top = 10.dp) - ) { - Text("Turn on") + WearFormScreen { + item { + Text( + text = "Bluetooth is off", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + } + item { + Button( + onClick = { launcher.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) }, + modifier = Modifier.padding(top = 10.dp), + ) { + Text("Turn on") + } } } } diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatActionBar.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatActionBar.kt index 72bdc8af..3f6deead 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatActionBar.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatActionBar.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -36,6 +37,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.unit.toSize import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -74,7 +76,7 @@ fun ChatActionBar( ) { Box( modifier = Modifier - .size(38.dp) + .size(48.dp) .clip(CircleShape) .background(palette.inputButton) .clickable { onKeyboard() }, @@ -89,7 +91,7 @@ fun ChatActionBar( } Box( modifier = Modifier - .size(38.dp) + .size(48.dp) .clip(CircleShape) .background( when { @@ -150,7 +152,7 @@ fun VoiceRecordOverlay( hoveringCancel: Boolean, proximity: Float, magnetPull: Offset, - onCancelBounds: (androidx.compose.ui.geometry.Rect) -> Unit + onCancelBounds: (androidx.compose.ui.geometry.Rect) -> Unit, ) { val palette = LocalBitchatPalette.current // The cancel morph, choreographed for feel: @@ -159,101 +161,127 @@ fun VoiceRecordOverlay( // - the button leans toward the approaching finger (magnetic pull), chasing it with a // smooth spring so it lags and settles naturally // - scale blooms with a soft bounce on activation — no rotation, no wobble - val cancelScale by androidx.compose.animation.core.animateFloatAsState( - targetValue = if (hoveringCancel) 1.32f else 1f + 0.1f * proximity, - animationSpec = androidx.compose.animation.core.spring( - dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy, - stiffness = androidx.compose.animation.core.Spring.StiffnessMedium - ), - label = "cancelSnap" - ) - val pull by androidx.compose.animation.core.animateOffsetAsState( - targetValue = magnetPull, - animationSpec = androidx.compose.animation.core.spring( - dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy, - stiffness = androidx.compose.animation.core.Spring.StiffnessMedium - ), - label = "magnetPull" - ) - val cancelColor = androidx.compose.ui.graphics.lerp( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.error, - if (hoveringCancel) 1f else proximity * 0.85f - ) + val cancelScale by + androidx.compose.animation.core.animateFloatAsState( + targetValue = if (hoveringCancel) 1.32f else 1f + 0.1f * proximity, + animationSpec = + androidx.compose.animation.core.spring( + dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy, + stiffness = androidx.compose.animation.core.Spring.StiffnessMedium, + ), + label = "cancelSnap", + ) + val pull by + androidx.compose.animation.core.animateOffsetAsState( + targetValue = magnetPull, + animationSpec = + androidx.compose.animation.core.spring( + dampingRatio = androidx.compose.animation.core.Spring.DampingRatioMediumBouncy, + stiffness = androidx.compose.animation.core.Spring.StiffnessMedium, + ), + label = "magnetPull", + ) + val cancelColor = + androidx.compose.ui.graphics.lerp( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.error, + if (hoveringCancel) 1f else proximity * 0.85f, + ) AnimatedVisibility( visible = voice.recording, enter = fadeIn(tween(BitchatMotion.EMPHASIZED_MS)), - exit = fadeOut(tween(BitchatMotion.EMPHASIZED_MS)) + exit = fadeOut(tween(BitchatMotion.EMPHASIZED_MS)), ) { - Column( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.96f)) - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + BoxWithConstraints( + modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center, ) { - Box( - modifier = Modifier - .onGloballyPositioned { coords -> - onCancelBounds( - androidx.compose.ui.geometry.Rect( - coords.localToRoot(androidx.compose.ui.geometry.Offset.Zero), - coords.size.toSize() - ) - ) - } - .size(52.dp) - .graphicsLayer { - translationX = pull.x - translationY = pull.y - scaleX = cancelScale - scaleY = cancelScale - } - .clip(CircleShape) - .background(cancelColor), - contentAlignment = Alignment.Center - ) { - androidx.compose.animation.Crossfade( - targetState = hoveringCancel, - animationSpec = tween(BitchatMotion.STANDARD_MS), - label = "cancelIcon" - ) { cancel -> - Icon( - imageVector = if (cancel) Icons.Filled.Close else Icons.Filled.Mic, - contentDescription = if (cancel) "cancel recording" else null, - tint = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.size(26.dp) - ) + val side = + if (LocalConfiguration.current.isScreenRound) { + roundContentSide(maxWidth.value, maxHeight.value).dp + } else { + minOf(maxWidth, maxHeight) - 24.dp } + Column( + modifier = Modifier.size(side), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + // Text gets its natural height first. The decorative waveform and cancel icon + // share the remaining room instead of pushing instructions off the display. + BoxWithConstraints( + Modifier.weight(1f).fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + val iconSize = minOf(52.dp, maxHeight / 1.32f, maxWidth / 1.32f) + Box( + modifier = + Modifier.onGloballyPositioned { coords -> + onCancelBounds( + androidx.compose.ui.geometry.Rect( + coords.localToRoot( + androidx.compose.ui.geometry.Offset.Zero + ), + coords.size.toSize(), + ) + ) + } + .size(iconSize) + .graphicsLayer { + translationX = pull.x + translationY = pull.y + scaleX = cancelScale + scaleY = cancelScale + } + .clip(CircleShape) + .background(cancelColor), + contentAlignment = Alignment.Center, + ) { + androidx.compose.animation.Crossfade( + targetState = hoveringCancel, + animationSpec = tween(BitchatMotion.STANDARD_MS), + label = "cancelIcon", + ) { cancel -> + Icon( + imageVector = if (cancel) Icons.Filled.Close else Icons.Filled.Mic, + contentDescription = if (cancel) "cancel recording" else null, + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(iconSize / 2), + ) + } + } + } + WaveformBars( + samples = voice.liveSamples, + progress = 1f, + activeColor = MaterialTheme.colorScheme.primary, + inactiveColor = MaterialTheme.colorScheme.primary, + modifier = Modifier.fillMaxWidth().height(12.dp), + ) + Text( + text = + (if (voice.isLive) "LIVE " else "") + + "%d:%02d" + .format( + voice.elapsedMs / 1000 / 60, + voice.elapsedMs / 1000 % 60, + ) + + "/0:10", + style = ChatVisualTokens.SystemActionStyle, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = if (hoveringCancel) "Release to cancel" else "Lift finger to send", + style = ChatVisualTokens.SystemActionStyle, + color = + if (hoveringCancel) MaterialTheme.colorScheme.error + else palette.textTertiary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) } - WaveformBars( - samples = voice.liveSamples, - progress = 1f, - activeColor = MaterialTheme.colorScheme.primary, - inactiveColor = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(top = 16.dp) - .fillMaxWidth() - .height(44.dp) - ) - Text( - text = (if (voice.isLive) "LIVE · " else "") + "%d:%02d".format( - voice.elapsedMs / 1000 / 60, - voice.elapsedMs / 1000 % 60 - ) + " / 0:10", - style = ChatVisualTokens.SenderStyle, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(top = 10.dp) - ) - Text( - text = if (hoveringCancel) "Release to cancel" else "Lift finger to send", - style = ChatVisualTokens.SystemActionStyle, - color = if (hoveringCancel) MaterialTheme.colorScheme.error - else palette.textTertiary, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 2.dp) - ) } } } diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index f304446e..20880cab 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -438,7 +438,7 @@ private fun ChatBody( // engages as the finger approaches, not only on exact contact. private const val CANCEL_HOVER_SLANT_PX = 56f private const val CHAT_SCROLL_DIRECTION_THRESHOLD_PX = 24 -private val CHAT_HEADER_CONTENT_CLEARANCE = 30.dp +private val CHAT_HEADER_CONTENT_CLEARANCE = 56.dp private val CHAT_ACTION_BAR_CLEARANCE = 64.dp private val CHAT_HEADER_EDGE_FADE = 36.dp private val CHAT_ACTION_BAR_EDGE_FADE = 72.dp diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt index d91f864b..d3f275e3 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt @@ -1,9 +1,7 @@ package com.bitchat.watch.ui -import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.verticalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -118,77 +116,79 @@ private fun ChatHeader( peerCount: Int, unreadDms: Int, expanded: Boolean, - onOpenPeople: () -> Unit + onOpenPeople: () -> Unit, ) { // Floating title row: full-size at the newest messages, shrinks to its dense form // while scrolling up into history. Rendered as an overlay, so the animation only // relayouts this row, never the message list. - val spec = androidx.compose.animation.core.tween( - BitchatMotion.STANDARD_MS - ) - val iconSize by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 16.dp else 11.dp, animationSpec = spec, label = "hdrIcon" - ) - val titleSize by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 15.dp else 11.dp, animationSpec = spec, label = "hdrTitle" - ) - val vPadding by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 6.dp else 1.dp, animationSpec = spec, label = "hdrPad" - ) + val spec = + androidx.compose.animation.core.tween( + BitchatMotion.STANDARD_MS + ) + val iconSize by + androidx.compose.animation.core.animateDpAsState( + targetValue = if (expanded) 16.dp else 11.dp, + animationSpec = spec, + label = "hdrIcon", + ) + val titleSize by + androidx.compose.animation.core.animateFloatAsState( + targetValue = if (expanded) 15f else 12f, + animationSpec = androidx.compose.animation.core.tween(BitchatMotion.STANDARD_MS), + label = "hdrTitle", + ) // The entire header region opens the People screen. When there are unread DMs the // title gives way so the people and mail icons (with counts) fit side by side on the // round screen instead of clipping at the edges. - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onOpenPeople() } - .padding(horizontal = 8.dp, vertical = vPadding), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + WearChatHeader( + fontSize = titleSize, + onClickLabel = "Open people", + onClick = onOpenPeople, ) { if (unreadDms == 0) { Text( text = "bitchat", style = MaterialTheme.typography.titleSmall, - fontSize = with(androidx.compose.ui.platform.LocalDensity.current) { titleSize.toSp() }, + fontSize = titleSize.sp, + lineHeight = (titleSize * 1.3f).sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(end = 8.dp) + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false).padding(end = 8.dp), ) } Icon( imageVector = Icons.Filled.People, contentDescription = "people", tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(iconSize) + modifier = Modifier.size(iconSize), ) Text( - text = "$peerCount", + text = if (peerCount > 99) "99+" else "$peerCount", style = MaterialTheme.typography.bodySmall, - fontSize = with(androidx.compose.ui.platform.LocalDensity.current) { - (iconSize.value * 0.85f).dp.toSp() - }, + fontSize = 12.sp, + lineHeight = (titleSize * 1.3f).sp, + maxLines = 1, color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 2.dp) + modifier = Modifier.padding(start = 2.dp), ) if (unreadDms > 0) { Icon( imageVector = Icons.Filled.MailOutline, contentDescription = "$unreadDms unread messages", tint = LocalBitchatPalette.current.accentOrange, - modifier = Modifier - .padding(start = 6.dp) - .size(iconSize) + modifier = Modifier.padding(start = 6.dp).size(iconSize), ) Text( - text = "$unreadDms", + text = if (unreadDms > 99) "99+" else "$unreadDms", style = MaterialTheme.typography.bodySmall, - fontSize = with(androidx.compose.ui.platform.LocalDensity.current) { - (iconSize.value * 0.85f).dp.toSp() - }, + fontSize = 12.sp, + lineHeight = (titleSize * 1.3f).sp, + maxLines = 1, color = LocalBitchatPalette.current.accentOrange, - modifier = Modifier.padding(start = 2.dp) + modifier = Modifier.padding(start = 2.dp), ) } } @@ -240,8 +240,7 @@ fun MessageItem( ) Text( text = " ${formatTime(message.timestamp)}", - style = ChatVisualTokens.SystemActionStyle, - fontSize = 9.sp, + style = ChatVisualTokens.TimestampStyle, color = palette.textTertiary ) } diff --git a/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt index 1d179a0b..eac4a5c9 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt @@ -1,11 +1,8 @@ package com.bitchat.watch.ui -import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.verticalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -20,7 +17,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -32,6 +28,8 @@ import androidx.wear.compose.foundation.rotary.rotaryScrollable 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.text.style.TextOverflow import androidx.compose.foundation.lazy.items import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.Icon @@ -140,59 +138,58 @@ private fun DmHeader( expanded: Boolean, isFavorite: Boolean, isVerified: Boolean, - onClick: () -> Unit + onClick: () -> Unit, ) { val palette = LocalBitchatPalette.current // Floating title row: full-size at the newest messages, shrinks to its dense form // while scrolling up into history. Rendered as an overlay, so the animation only // relayouts this row, never the message list. - val spec = androidx.compose.animation.core.tween( - BitchatMotion.STANDARD_MS - ) - val headerIconSize by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 16.dp else 11.dp, animationSpec = spec, label = "dmHdrIcon" - ) - val headerTitleSize by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 15.dp else 11.dp, animationSpec = spec, label = "dmHdrTitle" - ) - val headerVPadding by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 6.dp else 1.dp, animationSpec = spec, label = "dmHdrPad" - ) + val spec = + androidx.compose.animation.core.tween( + BitchatMotion.STANDARD_MS + ) + val headerIconSize by + androidx.compose.animation.core.animateDpAsState( + targetValue = if (expanded) 16.dp else 11.dp, + animationSpec = spec, + label = "dmHdrIcon", + ) + val headerTitleSize by + androidx.compose.animation.core.animateFloatAsState( + targetValue = if (expanded) 15f else 12f, + animationSpec = androidx.compose.animation.core.tween(BitchatMotion.STANDARD_MS), + label = "dmHdrTitle", + ) - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - onClickLabel = "Open user details", - onClick = onClick - ) - .padding(horizontal = 8.dp, vertical = headerVPadding), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + WearChatHeader( + fontSize = headerTitleSize, + onClickLabel = "Open user details", + onClick = onClick, ) { Text( text = nickname, style = MaterialTheme.typography.titleSmall, - fontSize = with(androidx.compose.ui.platform.LocalDensity.current) { - headerTitleSize.toSp() - }, + fontSize = headerTitleSize.sp, + lineHeight = (headerTitleSize * 1.3f).sp, fontWeight = FontWeight.Bold, - color = colorForPeer(nickname + peerID, palette) + color = colorForPeer(nickname + peerID, palette), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), ) NoiseLockIcon( - state = if (sessionEstablished) NoiseSessionUiState.Established - else NoiseSessionUiState.Handshaking, + state = + if (sessionEstablished) NoiseSessionUiState.Established + else NoiseSessionUiState.Handshaking, size = headerIconSize, - modifier = Modifier.padding(start = 5.dp) + modifier = Modifier.padding(start = 5.dp), ) if (isFavorite) { Icon( painter = painterResource(R.drawable.ic_spec_star_filled), contentDescription = "Favorite", tint = palette.accentOrange, - modifier = Modifier - .padding(start = 4.dp) - .size(headerIconSize) + modifier = Modifier.padding(start = 4.dp).size(headerIconSize), ) } if (isVerified) { @@ -200,9 +197,7 @@ private fun DmHeader( imageVector = Icons.Filled.Verified, contentDescription = "Verified", tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(start = 4.dp) - .size(headerIconSize) + modifier = Modifier.padding(start = 4.dp).size(headerIconSize), ) } } diff --git a/wear/src/main/java/com/bitchat/watch/ui/NicknameSetupScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/NicknameSetupScreen.kt index 68710025..b49e634d 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/NicknameSetupScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/NicknameSetupScreen.kt @@ -1,10 +1,7 @@ package com.bitchat.watch.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -47,7 +44,7 @@ fun NicknameSetupScreen( title: String = "bitchat", subtitle: String = "Pick a nickname", confirmLabel: String = "Join the mesh", - onConfirm: (String) -> Unit + onConfirm: (String) -> Unit, ) { val palette = LocalBitchatPalette.current // Pre-fill with the cursor at the end of the existing name, not the start. @@ -55,7 +52,7 @@ fun NicknameSetupScreen( mutableStateOf( TextFieldValue( text = initialNickname, - selection = TextRange(initialNickname.length) + selection = TextRange(initialNickname.length), ) ) } @@ -64,71 +61,78 @@ fun NicknameSetupScreen( LaunchedEffect(Unit) { focusRequester.requestFocus() } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = subtitle, - style = MaterialTheme.typography.bodySmall, - color = palette.textTertiary, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 4.dp, bottom = 10.dp) - ) - BasicTextField( - value = name, - onValueChange = { newValue -> - val trimmed = newValue.text.trim().take(24) - name = if (trimmed == newValue.text) { - newValue - } else { - newValue.copy(text = trimmed, selection = TextRange(trimmed.length)) - } - }, - singleLine = true, - textStyle = ChatVisualTokens.MessageBodyStyle.copy( - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center - ), - cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .clip(RoundedCornerShape(18.dp)) - .background(palette.inputSurface) - .padding(horizontal = 12.dp, vertical = 8.dp), - decorationBox = { innerTextField -> - Box(contentAlignment = Alignment.Center) { - if (name.text.isEmpty()) { - Text( - text = "Nickname", - style = ChatVisualTokens.MessageBodyStyle, - color = palette.textTertiary - ) + WearFormScreen { + item { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + ) + } + item { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = palette.textTertiary, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 4.dp, bottom = 10.dp), + ) + } + item { + BasicTextField( + value = name, + onValueChange = { newValue -> + val trimmed = newValue.text.trim().take(24) + name = + if (trimmed == newValue.text) { + newValue + } else { + newValue.copy(text = trimmed, selection = TextRange(trimmed.length)) + } + }, + singleLine = true, + textStyle = + ChatVisualTokens.MessageBodyStyle.copy( + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = + KeyboardActions( + onDone = { + keyboardController?.hide() + } + ), + modifier = + Modifier.fillMaxWidth() + .focusRequester(focusRequester) + .clip(RoundedCornerShape(18.dp)) + .background(palette.inputSurface) + .padding(horizontal = 12.dp, vertical = 8.dp), + decorationBox = { innerTextField -> + Box(contentAlignment = Alignment.Center) { + if (name.text.isEmpty()) { + Text( + text = "Nickname", + style = ChatVisualTokens.MessageBodyStyle, + color = palette.textTertiary, + ) + } + innerTextField() } - innerTextField() - } + }, + ) + } + item { + Button( + onClick = { if (name.text.isNotBlank()) onConfirm(name.text.trim()) }, + enabled = name.text.isNotBlank(), + modifier = Modifier.padding(top = 10.dp), + ) { + Text(confirmLabel, textAlign = TextAlign.Center) } - ) - Button( - onClick = { if (name.text.isNotBlank()) onConfirm(name.text.trim()) }, - enabled = name.text.isNotBlank(), - modifier = Modifier.padding(top = 10.dp) - ) { - Text(confirmLabel) } } } diff --git a/wear/src/main/java/com/bitchat/watch/ui/TextInputScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/TextInputScreen.kt index f73891f9..df1b92a2 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/TextInputScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/TextInputScreen.kt @@ -134,7 +134,7 @@ fun TextInputScreen(onSend: (String) -> Unit) { } ) }, - modifier = Modifier.size(38.dp) + modifier = Modifier.size(48.dp) ) { Icon( imageVector = Icons.Filled.Mic, @@ -145,7 +145,7 @@ fun TextInputScreen(onSend: (String) -> Unit) { IconButton( onClick = { send() }, enabled = text.isNotBlank(), - modifier = Modifier.size(38.dp) + modifier = Modifier.size(48.dp) ) { Icon( imageVector = Icons.AutoMirrored.Filled.Send, diff --git a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt index 75f2db9c..fd6a347f 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/UserDetailScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState @@ -45,7 +44,7 @@ fun UserDetailScreen( WearPeerIdentityState.snapshot(peerID, mesh) } val nickname = mesh?.getPeerNickname(peerID) ?: peerID.take(8) - val listState = rememberScalingLazyListState() + val listState = rememberScalingLazyListState(initialCenterItemIndex = 0) val palette = LocalBitchatPalette.current ScreenScaffold(scrollState = listState) { scaffoldPadding -> @@ -53,12 +52,13 @@ fun UserDetailScreen( ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), + autoCentering = null, contentPadding = scaffoldPadding .withAdditionalPadding( layoutDirection = layoutDirection, - horizontal = 10.dp, - vertical = 8.dp + horizontal = 10.dp ) + .withVerticalClearance(layoutDirection, top = 28.dp, bottom = 28.dp) ) { item { ListHeader { @@ -71,8 +71,7 @@ fun UserDetailScreen( style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold, color = colorForPeer(nickname + peerID, palette), - maxLines = 1, - overflow = TextOverflow.Ellipsis + textAlign = TextAlign.Center ) Text( text = "User details", @@ -174,13 +173,13 @@ fun UserDetailScreen( text = if (identity.isVerified) { "Identity verified" } else { - "Verification code" + "Identity code" }, style = ChatVisualTokens.SenderStyle, color = MaterialTheme.colorScheme.onSurface ) Text( - text = "Compare cryptographic fingerprints", + text = "Compare identity codes", style = ChatVisualTokens.SystemActionStyle, color = palette.textTertiary ) diff --git a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt index 104ab660..4644e9bd 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/VerificationCodeScreen.kt @@ -40,7 +40,7 @@ fun VerificationCodeScreen(peerID: String) { WearPeerIdentityState.snapshot(peerID, mesh) } val myFingerprint = WearPeerIdentityState.myFingerprint(mesh) - val listState = rememberScalingLazyListState() + val listState = rememberScalingLazyListState(initialCenterItemIndex = 0) val palette = LocalBitchatPalette.current ScreenScaffold(scrollState = listState) { scaffoldPadding -> @@ -48,12 +48,13 @@ fun VerificationCodeScreen(peerID: String) { ScalingLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), + autoCentering = null, contentPadding = scaffoldPadding .withAdditionalPadding( layoutDirection = layoutDirection, - horizontal = 10.dp, - vertical = 8.dp + horizontal = 10.dp ) + .withVerticalClearance(layoutDirection, top = 28.dp, bottom = 28.dp) ) { item { ListHeader { @@ -126,7 +127,8 @@ fun VerificationCodeScreen(peerID: String) { "Remove verification" } else { "Mark verified" - } + }, + textAlign = TextAlign.Center ) } } @@ -158,8 +160,8 @@ private fun FingerprintCard( text = fingerprint?.let(::formatVerificationCode) ?: "Handshake pending", style = MaterialTheme.typography.bodySmall.copy( fontFamily = FontFamily.Monospace, - fontSize = 10.sp, - lineHeight = 13.sp + fontSize = 12.sp, + lineHeight = 16.sp ), color = if (fingerprint == null) { palette.accentOrange @@ -178,6 +180,6 @@ fun formatVerificationCode(fingerprint: String): String { return fingerprint .uppercase() .chunked(4) - .chunked(4) + .chunked(2) .joinToString("\n") { line -> line.joinToString(" ") } } diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt b/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt new file mode 100644 index 00000000..b30ab885 --- /dev/null +++ b/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt @@ -0,0 +1,71 @@ +package com.bitchat.watch.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.wear.compose.material3.MaterialTheme + +/** The title is outside the scrolling list, so it needs its own physical display bounds. */ +@Composable +internal fun WearChatHeader( + fontSize: Float, + onClickLabel: String, + onClick: () -> Unit, + content: @Composable RowScope.() -> Unit, +) { + val configuration = LocalConfiguration.current + val lineHeight = with(LocalDensity.current) { (fontSize * 1.3f).sp.toDp() } + val rowHeight = maxOf(48.dp, lineHeight) + val top = 12.dp + (rowHeight - lineHeight) / 2 + BoxWithConstraints( + Modifier.fillMaxWidth() + .background( + Brush.verticalGradient( + 0f to MaterialTheme.colorScheme.background, + 0.75f to MaterialTheme.colorScheme.background, + 1f to Color.Transparent, + ) + ), + contentAlignment = Alignment.TopCenter, + ) { + val safeWidth = + if (configuration.isScreenRound) { + roundBandWidth( + maxWidth.value, + configuration.screenHeightDp.toFloat(), + top.value, + (top + lineHeight).value, + ) + .dp - 8.dp + } else { + maxWidth - 16.dp + } + Row( + modifier = + Modifier.padding(top = 12.dp) + .width(safeWidth.coerceAtLeast(0.dp)) + .height(rowHeight) + .clickable(role = Role.Button, onClickLabel = onClickLabel, onClick = onClick), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = content, + ) + } +} diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearDisplayGeometry.kt b/wear/src/main/java/com/bitchat/watch/ui/WearDisplayGeometry.kt new file mode 100644 index 00000000..894e43a6 --- /dev/null +++ b/wear/src/main/java/com/bitchat/watch/ui/WearDisplayGeometry.kt @@ -0,0 +1,16 @@ +package com.bitchat.watch.ui + +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.sqrt + +/** Width of the narrowest chord across a centered horizontal band in a round display. */ +internal fun roundBandWidth(width: Float, height: Float, top: Float, bottom: Float): Float { + val radius = min(width, height) / 2f + val distance = maxOf(abs(top - height / 2f), abs(bottom - height / 2f)) + return 2f * sqrt((radius * radius - distance * distance).coerceAtLeast(0f)) +} + +/** A centered square whose four corners fit inside the physical circle, with a small inset. */ +internal fun roundContentSide(width: Float, height: Float): Float = + (min(width, height) / sqrt(2f) - 4f).coerceAtLeast(0f) diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearFormScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/WearFormScreen.kt new file mode 100644 index 00000000..75cf3703 --- /dev/null +++ b/wear/src/main/java/com/bitchat/watch/ui/WearFormScreen.kt @@ -0,0 +1,30 @@ +package com.bitchat.watch.ui + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.foundation.lazy.ScalingLazyListScope +import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState +import androidx.wear.compose.material3.ScreenScaffold + +/** Independently scrollable form items remain reachable on small watches and at large fonts. */ +@Composable +internal fun WearFormScreen(content: ScalingLazyListScope.() -> Unit) { + val state = rememberScalingLazyListState(initialCenterItemIndex = 0) + val direction = LocalLayoutDirection.current + ScreenScaffold(scrollState = state) { padding -> + ScalingLazyColumn( + state = state, + modifier = Modifier.fillMaxSize(), + autoCentering = null, + contentPadding = + padding + .withAdditionalPadding(direction, horizontal = 14.dp) + .withVerticalClearance(direction, top = 28.dp, bottom = 28.dp), + content = content, + ) + } +} diff --git a/wear/src/main/java/com/bitchat/watch/ui/media/MediaItems.kt b/wear/src/main/java/com/bitchat/watch/ui/media/MediaItems.kt index e27f132e..707132de 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/media/MediaItems.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/media/MediaItems.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio @@ -43,6 +44,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -53,6 +55,7 @@ import androidx.wear.compose.material3.Text import com.bitchat.android.features.voice.AudioWaveformExtractor import com.bitchat.android.features.voice.VoiceWaveformCache import com.bitchat.watch.ui.theme.ChatVisualTokens +import com.bitchat.watch.ui.roundContentSide import com.bitchat.watch.ui.theme.LocalBitchatPalette import kotlinx.coroutines.delay import java.io.File @@ -92,7 +95,7 @@ fun FullScreenImageViewer(path: String, onClose: () -> Unit) { onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false) ) { - Box( + BoxWithConstraints( modifier = Modifier .fillMaxSize() .background(Color.Black) @@ -101,11 +104,16 @@ fun FullScreenImageViewer(path: String, onClose: () -> Unit) { ) { val bitmap = remember(path) { BitmapFactory.decodeFile(path) } if (bitmap != null) { + val imageModifier = if (LocalConfiguration.current.isScreenRound) { + Modifier.size(roundContentSide(maxWidth.value, maxHeight.value).dp) + } else { + Modifier.fillMaxSize() + } Image( painter = BitmapPainter(bitmap.asImageBitmap()), contentDescription = "image fullscreen", contentScale = ContentScale.Fit, - modifier = Modifier.fillMaxSize() + modifier = imageModifier ) } Icon( @@ -114,7 +122,7 @@ fun FullScreenImageViewer(path: String, onClose: () -> Unit) { tint = Color.White.copy(alpha = 0.7f), modifier = Modifier .align(Alignment.TopCenter) - .padding(top = 24.dp) + .padding(top = 8.dp) .size(20.dp) ) } diff --git a/wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt b/wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt index ff66567d..aca6c631 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/theme/Typography.kt @@ -37,7 +37,9 @@ object ChatVisualTokens { val SystemActionStyle = TextStyle( fontFamily = BitchatFontFamily, fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 14.sp, + fontSize = 12.sp, + lineHeight = 15.sp, ) + + val TimestampStyle = SystemActionStyle.copy(fontSize = 10.sp, lineHeight = 12.sp) } diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt new file mode 100644 index 00000000..96bc4e99 --- /dev/null +++ b/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt @@ -0,0 +1,47 @@ +package com.bitchat.watch.ui + +import com.bitchat.watch.ui.theme.ChatVisualTokens +import kotlin.math.pow +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class WearDisplayGeometryTest { + @Test + fun `header band corners stay inside the circle at supported text sizes`() { + for (diameter in listOf(192f, 228f, 240f)) { + for (scale in listOf(0.94f, 1f, 1.24f, 1.3f)) { + val lineHeight = 15f * 1.3f * scale + val top = 12f + (maxOf(48f, lineHeight) - lineHeight) / 2f + val width = roundBandWidth(diameter, diameter, top, top + lineHeight) + val radius = diameter / 2f + for (y in listOf(top, top + lineHeight)) { + assertTrue((width / 2).pow(2) + (y - radius).pow(2) <= radius.pow(2) + 0.01f) + } + } + } + } + + @Test + fun `out of display bands have no usable width`() { + assertEquals(0f, roundBandWidth(192f, 192f, -1f, 20f)) + assertEquals(0f, roundBandWidth(192f, 192f, 180f, 193f)) + } + + @Test + fun `image and recording safe square fits all four corners`() { + for (diameter in listOf(192f, 228f, 240f)) { + val side = roundContentSide(diameter, diameter) + assertTrue(side > 0) + assertTrue(2 * (side / 2).pow(2) < (diameter / 2).pow(2)) + } + } + + @Test + fun `essential shared text is at least twelve sp`() { + assertTrue(ChatVisualTokens.SystemActionStyle.fontSize.value >= 12f) + assertTrue(ChatVisualTokens.MessageBodyStyle.fontSize.value >= 12f) + assertTrue(ChatVisualTokens.SenderStyle.fontSize.value >= 12f) + assertTrue(ChatVisualTokens.TimestampStyle.fontSize.value >= 10f) + } +} diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearPeerIdentityStateTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearPeerIdentityStateTest.kt index 1b241690..9e26b1ef 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearPeerIdentityStateTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearPeerIdentityStateTest.kt @@ -32,7 +32,7 @@ class WearPeerIdentityStateTest { val formatted = formatVerificationCode(fingerprint) assertEquals(fingerprint.uppercase(), formatted.filterNot(Char::isWhitespace)) - assertEquals(4, formatted.lines().size) - assertEquals(listOf(4, 4, 4, 4), formatted.lines().map { it.split(" ").size }) + assertEquals(8, formatted.lines().size) + assertEquals(List(8) { 2 }, formatted.lines().map { it.split(" ").size }) } } From 9a399cc3332157ab340b18cc9c115a98bb760ebb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 6 Sep 2026 06:16:30 +0000 Subject: [PATCH 40/45] Automated update of relay data - Sun Sep 6 06:16:30 UTC 2026 --- app/src/main/assets/nostr_relays.csv | 757 +++++++++++++-------------- 1 file changed, 373 insertions(+), 384 deletions(-) diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index 61c0c05c..9913b970 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,389 +1,378 @@ Relay URL,Latitude,Longitude -dm-test-strfry-generic.samt.st,43.6532,-79.3832 -nostrelay.circum.space:443,52.2245,8.826 -relay.staging.plebeian.market,51.5072,-0.127586 -nostr.chaima.info:443,50.1109,8.68213 -relay.vrtmrz.net:443,43.6532,-79.3832 -relay.gulugulu.moe:443,43.6532,-79.3832 -relay.dreamith.to,43.6532,-79.3832 -nostrride.io,37.3986,-121.964 -nostr.n7ekb.net,47.4941,-122.294 -relay.npubhaus.com,43.6532,-79.3832 -relay1.privkey.io,25.7617,-80.1918 -nostr-relay.xbytez.io:443,50.6924,3.20113 -relay.bullishbounty.com,43.6532,-79.3832 -nostr.islandarea.net,35.4669,-97.6473 -nostrrelay.taylorperron.com,45.5029,-73.5723 -relay.sharegap.net,43.6532,-79.3832 -relayone.geektank.ai,39.0997,-94.5786 -nexus.libernet.app,43.6532,-79.3832 -fanfares.nostr1.com:443,40.7057,-74.0136 -relay.satlantis.io,39.0438,-77.4874 -relay.hivetalk.org,40.8302,-74.1299 -nos.lol,50.4754,12.3683 -staging.yabu.me,35.6092,139.73 -relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -relay.internationalright-wing.org,-22.4692,-48.9875 -relay.nostrcheck.me,43.6532,-79.3832 -denimroad.feeds.relay.tools,38.6327,-90.1961 -relay.nostr-check.me,43.6532,-79.3832 -ribo.us.nostria.app:443,43.6532,-79.3832 -relay.lightning.pub,39.0438,-77.4874 -myvoiceourstory.org,37.3598,-121.981 -relay.jmoose.rocks:443,37.4419,-122.143 -nostr-pr02.redscrypt.org,52.3676,4.90414 -nostr.purpura.cloud,43.6532,-79.3832 -relay.sigit.io:443,50.4754,12.3683 -relay.nostrops.com,32.71,-96.6745 -relay.jmoose.rocks,37.4419,-122.143 -nostr.spaceshell.xyz,43.6532,-79.3832 -nostr-pub.wellorder.net,45.5201,-122.99 -relay.opmaat.org,60.1699,24.9384 -nostr.myshosholoza.co.za,52.3676,4.90414 -offchain.bostr.online,43.6532,-79.3832 -nostr-relay.nilpote.com,43.6532,-79.3832 -relay.nostu.be,40.4167,-3.70329 -nostr.stakey.net,52.3676,4.90414 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -conduitl2.fly.dev,38.7946,-106.535 -vault.iris.to:443,43.6532,-79.3832 -relay.minibolt.info,43.6532,-79.3832 -articles.layer3.news:443,37.3387,-121.885 -nostr.overmind.lol,43.6532,-79.3832 -nostr.21crypto.ch,47.5356,8.73209 -nostr.tabordalab.com,60.1699,24.9384 -nostr.spicyz.io,43.6532,-79.3832 -nostr.sathoarder.com,48.5734,7.75211 -relay.chorus.community,48.5333,10.7 -dev.relay.stream,43.6532,-79.3832 -nostr-relay.corb.net:443,39.6478,-104.988 -relay.lightning.pub:443,39.0438,-77.4874 -nostr.tbxnetworx.de,47.8226,10.0064 -nostr.azzamo.net,52.2633,21.0283 -relay.loveisbitcoin.com,43.6532,-79.3832 -strfry.ymir.cloud,43.6532,-79.3832 -chorus.mikedilger.com:444,-36.8906,174.794 -relay.agilesolutionlabs.com,43.6532,-79.3832 -relay.gulugulu.moe,43.6532,-79.3832 -nostr.tac.lol,47.4748,-122.273 -relay.mypathtofire.de,42.8864,-78.8784 -nostrelay.circum.space,52.2245,8.826 -bcast.girino.org,43.6532,-79.3832 -relay.ordoplay.com,50.1109,8.68213 -nostr.chaima.info,50.1109,8.68213 -offchain.pub:443,39.1585,-94.5728 -relay.wisp.talk,49.4543,11.0746 -strfry.bonsai.com,39.0438,-77.4874 -nostr.2b9t.xyz:443,34.0549,-118.243 -relay.nostx.io,43.6532,-79.3832 -bucket.coracle.social,37.7775,-122.397 -relay.wavlake.com:443,41.2619,-95.8608 -nostr.carroarmato0.be:443,50.914,3.21378 -treuzkas.branruz.com,48.8575,2.35138 -nostr.computingcache.com,45.5341,-122.956 -cs-relay.nostrdev.com,50.4754,12.3683 -relay.lanacoin-eternity.com,40.8302,-74.1299 -nostr.rtvslawenia.com,49.4543,11.0746 -nip85.nosfabrica.com,39.0997,-94.5786 -relay.wisp.talk:443,49.4543,11.0746 -nostr.stakey.net:443,52.3676,4.90414 -relay.piazza.today,48.122,11.589 -relay.conduit.market,38.7946,-106.535 -relay.nostrmap.net,60.1699,24.9384 -maxq.descendant.io,43.6532,-79.3832 -nostr.hekster.org,37.3986,-121.964 -nostr.christiansass.de,49.7423,8.76687 -tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -relay.scuba323.com,40.8218,-74.45 -schnorr.me,43.6532,-79.3832 -relay.cypherflow.ai,48.8575,2.35138 -nostr.snowbla.de:443,50.4754,12.3683 -nostr.whitenode45.ddns.net,40.55,-74.4758 -adre.su,59.9311,30.3609 -relay.getsafebox.app,43.6532,-79.3832 -auth.nostr1.com,40.7057,-74.0136 -relay2.veganostr.com,60.1699,24.9384 -relay.i9.eti.br,43.6532,-79.3832 -relay.satsmarkt.club,52.6907,4.8181 -nostr.bitcoiner.social:443,47.6743,-117.112 -cache.trustr.ing,43.6548,-79.3885 -relay.mitchelltribe.com,39.0438,-77.4874 -21milionidinostr.duckdns.org,41.8967,12.4822 -public.crostr.com:443,43.6532,-79.3832 -relay.otrta.me,50.1109,8.68213 -relay.bornheimer.app,50.1109,8.68213 -relay.sigit.io,50.4754,12.3683 -nostr-relay.xbytez.io,50.6924,3.20113 -relayone.soundhsa.com,39.0997,-94.5786 -vm-1734.lnvps.cloud,53.3498,-6.26031 -nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 -nostr.twinkle.lol,51.902,7.6657 -nostr.data.haus,50.4754,12.3683 -nostr.mas-family.eu,60.3478,15.7505 -relay.nostrhub.fr,48.1045,11.6004 -nostr.openhoofd.nl,51.5717,3.70417 -relay.nostreon.com,60.1699,24.9384 -relay.tdw.lol,41.8781,-87.6298 -top.testrelay.top,43.6532,-79.3832 -syb.lol,34.0549,-118.243 -relay.primal.net,43.6532,-79.3832 -n.musicroadmap.com,35.694,139.754 -ribo.eu.nostria.app,43.6532,-79.3832 -nostr.red5d.dev,43.6532,-79.3832 -nostr.ltd,19.076,72.8777 -relay.olas.app,60.1699,24.9384 -chat-relay.zap-work.com,43.6532,-79.3832 -relay.islandbitcoin.com,12.8498,77.6545 -relay.chatbett.de,40.7128,-74.006 -relay.novospes.com,43.6532,-79.3832 -nostr.bond,50.1109,8.68213 -inbox.scuba323.com,40.8218,-74.45 -0x-nostr-relay.fly.dev,38.7946,-106.535 -relay.sincensura.org,43.6532,-79.3832 -nostr-02.uid.ovh,50.9871,2.12554 -relay.solomonstr.com,43.6532,-79.3832 -purplerelay.com,43.6532,-79.3832 -nostr.emanuelemiani.it,45.778,8.79414 -relay.damustr.com,43.6532,-79.3832 -fanfares.nostr1.com,40.7057,-74.0136 -relay.plebeian.market:443,50.1109,8.68213 -buzz.ac2n-share.kozow.com,45.764,4.83566 -nostr-01.yakihonne.com:443,1.32123,103.695 -relay.snotr.nl:49999,51.9758,4.31389 -relay.ltgnet.work:8443,51.0511,-114.075 -nostras.app,38.7946,-106.535 -purplerelay.com:443,43.6532,-79.3832 -relay.bnos.space,43.6532,-79.3832 -porchlight.social,43.6532,-79.3832 -relay.mostr.pub:443,43.6532,-79.3832 -relay.layer.systems,49.0291,8.35695 -nostrcity-club.fly.dev,38.7946,-106.535 -relay.guggero.org,46.5971,9.59652 -relay.layer.systems:443,49.0291,8.35695 -relay.edufeed.org,49.4521,11.0767 -nostr.mom:443,50.4754,12.3683 -relay.mccormick.cx,52.3563,4.95714 -public.crostr.com,43.6532,-79.3832 -mostro-p2p.tech,50.1109,8.68213 -strfry.shock.network:443,39.0438,-77.4874 -relay.agentry.com,42.8864,-78.8784 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 -relay.nostrian-conquest.com,41.223,-111.974 -relay.fountain.fm,43.6532,-79.3832 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -rele.speyhard.fi,50.1109,8.68213 -reraw.pbla2fish.cc,43.6532,-79.3832 -relay.pyramid.li,47.4093,8.46503 -relay.trotters.cc,43.6532,-79.3832 -relay.ru.ac.th,13.7607,100.627 -relay.orly.dev,32.7767,-96.797 -nostr.yutakobayashi.com,43.6532,-79.3832 -nostr-01.yakihonne.com,1.32123,103.695 -relay.lanavault.space,60.1699,24.9384 -chat-relay.zap-work.com:443,43.6532,-79.3832 -strfry.shock.network,39.0438,-77.4874 -wheat.happytavern.co,43.6532,-79.3832 -relay.klabo.world,47.674,-122.122 -relay.littlebitstudios.com,43.6532,-79.3832 -nostr-relay.corb.net,39.6478,-104.988 -relay.flashapp.me,43.6548,-79.3885 -espelho.girino.org,43.6532,-79.3832 -budabit.nostr1.com,40.7057,-74.0136 -nostr.vulpem.com,49.4543,11.0746 -relay.nostrfeed.com,60.1699,24.9384 -basspistol.org,49.0291,8.35696 -relay-rpi.edufeed.org,49.4521,11.0767 -relay.mitchelltribe.com:443,39.0438,-77.4874 -spamspamspamspam.rest,43.6532,-79.3832 -relayrs.notoshi.win:443,43.6532,-79.3832 -relay.endfiat.money,59.3327,18.0656 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -nostr.tagomago.me,42.3601,-71.0589 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -nostr.rtvslawenia.com:443,49.4543,11.0746 -no.str.cr:443,8.96171,-83.5246 -relay.ditto.pub,43.6532,-79.3832 -relay.shadowbip.com,54.352,18.6466 -nostr.liberty.fans,36.8767,-89.5879 -prl.plus,55.7628,37.5983 -relay.agorist.space,52.3734,4.89406 -articles.layer3.news,37.3387,-121.885 -nostr.thalheim.io:443,60.1699,24.9384 -relay.mccormick.cx:443,52.3563,4.95714 -support.flotilla.social,32.9483,-96.7299 -nostr.mikoshi.de,50.1109,8.68213 -nostrmxn.lulus.com.mx,59.4016,17.9455 -bridge.tagomago.me,42.3601,-71.0589 -relayrs.notoshi.win,43.6532,-79.3832 -nostr.88mph.life,52.1941,-2.21905 -nostr.fullstackcash.net,45.5201,-122.99 -relay5.bitransfer.org,43.6532,-79.3832 -relay.0xchat.com,43.6532,-79.3832 -relay.nostriot.com,41.5695,-83.9786 -relay.wellorder.net,45.5201,-122.99 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -nostr.relay.hedwig.sh,60.1699,24.9384 -relay.openspecs.uid.ovh,50.9871,2.12554 -relay.keykeeper.world,40.7824,-74.0711 -nostr.wecsats.io:443,43.6532,-79.3832 -relay.wavlake.com,41.2619,-95.8608 -relay.lacrypta.ar,43.6532,-79.3832 -nos.lol:443,50.4754,12.3683 -dm-test-strfry-discovery.samt.st:443,43.6532,-79.3832 -relay.nmail.li,50.9871,2.12554 -relay.mostro.network,40.8302,-74.1299 -bitcoinostr.duckdns.org,41.1976,1.11167 -relay.s-w.art,43.6532,-79.3832 +relayone.geektank.ai:443,39.0997,-94.5786 relay.ohstr.com:443,43.6532,-79.3832 +budabit.nostr1.com,40.7057,-74.0136 +relay.flashapp.me,43.6548,-79.3885 +relay.mccormick.cx:443,52.3563,4.95714 +nostr.mom:443,50.4754,12.3683 +nostr.carroarmato0.be,51.0355,4.48285 +relay01.lnfi.network,35.6764,139.65 +relay.mwaters.net,50.9871,2.12554 +cyberspace.nostr1.com,40.7057,-74.0136 +relay.endfiat.money:443,57.7089,11.9746 +nostr.mas-family.eu,60.3478,15.7505 +nostr.hekster.org:443,37.3986,-121.964 +relay.favillakey.io,-27.4705,153.026 +21milionidinostr.duckdns.org,43.3054,13.5808 +staging.yabu.me,35.6092,139.73 +relay.lanacoin-eternity.com,40.8302,-74.1299 +relay.pocketstr.com,40.8302,-74.1299 +relay.0xchat.com,43.6532,-79.3832 +vault.iris.to,43.6532,-79.3832 +nexus.libernet.app,43.6532,-79.3832 +relay.conduit.market,38.7946,-106.535 +aurum.saturnali.net,46.2044,6.14316 +relay.loveisbitcoin.com,43.6532,-79.3832 +nostr.unkn0wn.world,46.8499,9.53287 +relay.nmail.li,50.9871,2.12554 +relay.nostrmap.net:443,60.1699,24.9384 +relay.kizuna-miki.com,35.694,139.754 +nostras.app,38.7946,-106.535 +nostr.pbfs.io:443,50.4754,12.3683 +relay.tdw.lol,41.8781,-87.6298 +relayrs.notoshi.win,43.6532,-79.3832 +relay.nostrhub.fr,48.1045,11.6004 +nostr.infero.net,35.6764,139.65 +relay5.bitransfer.org,43.6532,-79.3832 +adre.su,59.9311,30.3609 +buzz.ac2n-share.kozow.com,45.764,4.83566 +0x-nostr-relay.fly.dev,38.7946,-106.535 +relay.satlantis.io,39.0438,-77.4874 +relay.nostr.bond,40.1885,29.061 +nostr.relay.hedwig.sh,60.1699,24.9384 +relay.lightning.pub:443,39.0438,-77.4874 +relay.edufeed.org:443,49.4521,11.0767 +nostr.dlcdevkit.com,40.0992,-83.1141 +nostr.dlcdevkit.com:443,40.0992,-83.1141 +nostr.pbfs.io,50.4754,12.3683 +soloco.nl,43.6532,-79.3832 +chat-relay.zap-work.com:443,43.6532,-79.3832 +nostr.purpura.cloud,43.6532,-79.3832 +relay.nostrmap.net,60.1699,24.9384 +nostr.plantroon.com:443,50.1013,8.62643 +nostr.novacisko.cz,52.2026,20.9397 +relay.bitmacro.cloud,43.6532,-79.3832 +bitcoinostr.duckdns.org,41.1976,1.11167 +nostr.l484.com,30.2672,-97.7431 +relay.sincensura.org,43.6532,-79.3832 +nostr-relay.acloud.kdns.fr,43.6532,-79.3832 +dev-relay.nostr.space,43.6532,-79.3832 +bruh.samt.st,43.6532,-79.3832 +nostrcity-club.fly.dev:443,38.7946,-106.535 +relay.beginningend.com,35.2227,-97.4786 +relay.i9.eti.br,43.6532,-79.3832 +relay.kaleidoswap.com,50.8476,4.35717 +relay2.angor.io,48.1046,11.6002 +relay.wellorder.net,45.5201,-122.99 +nostr.2b9t.xyz:443,34.0549,-118.243 +bcast.seutoba.com.br,43.6532,-79.3832 +relay.chorus.community:443,50.7529,6.84585 +nostr.janx.com,43.6532,-79.3832 +nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 +cs-relay.nostrdev.com:443,50.4754,12.3683 +prl.plus,55.7558,37.6173 +nostr.spaceshell.xyz,43.6532,-79.3832 +relay.nostreon.com,60.1699,24.9384 +relay.nostr.net,43.6532,-79.3832 +nos.lol:443,50.4754,12.3683 +articles.layer3.news:443,37.3387,-121.885 +relay.manneken.brussels,49.4543,11.0746 +bucket.coracle.social,37.7775,-122.397 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +relay.getvia.xyz,60.1699,24.9384 +nostrmxn.lulus.com.mx,59.4016,17.9455 +nostr.oxtr.dev,50.4754,12.3683 +relay.mitosystem.net,-23.5558,-46.6396 +relayone.soundhsa.com:443,39.0997,-94.5786 +relay.nuts.cash,52.3676,4.90414 +nostr.snowbla.de:443,50.4754,12.3683 +strfry.bonsai.com,39.0438,-77.4874 +relay.edufeed.org,49.4521,11.0767 +nostr.sathoarder.com,48.5734,7.75211 +relay.paulstephenborile.com,49.4543,11.0746 +x.kojira.io,43.6532,-79.3832 +relay.keykeeper.world,40.7824,-74.0711 +relay.qstr.app,50.1109,8.68213 +nostr.snowbla.de,50.4754,12.3683 +wisp.djorivaltech.com,43.6532,-79.3832 +relayone.geektank.ai,39.0997,-94.5786 +relay.ditto.pub:443,43.6532,-79.3832 +relay.staging.plebeian.market,51.5072,-0.127586 +relay.nostrcheck.me,43.6532,-79.3832 +nostr.planix.org,43.6532,-79.3832 +nostr.plantroon.com,50.1013,8.62643 +nostr.islandarea.net,35.4669,-97.6473 +relay.nostrfeed.com,60.1699,24.9384 +relay.mitchelltribe.com,39.0438,-77.4874 +relay.fiatdenier.com,43.7787,-79.5393 +nostr-relay.xbytez.io,50.6924,3.20113 +nostr.hekster.org,37.3986,-121.964 +relay.laantungir.net,-19.4692,-42.5315 +nostr.openhoofd.nl,51.5717,3.70417 +nostr.computingcache.com,45.5341,-122.956 +relay.endfiat.money,57.7089,11.9746 +dev.relay.stream,43.6532,-79.3832 +nostr.wecsats.io,43.6532,-79.3832 +relay-dev.gulugulu.moe:443,43.6532,-79.3832 +relay.nostu.be,40.4167,-3.70329 +nostr.rtvslawenia.com:443,49.4543,11.0746 +nostr.ltd,19.076,72.8777 +relay.veganostr.com:443,60.1699,24.9384 +relay.shadowbip.com,54.352,18.6466 +denimroad.feeds.relay.tools,38.6327,-90.1961 +relay.arx-ccn.com,50.4754,12.3683 +nostr-relay.corb.net,39.6478,-104.988 +relay.cosmicbolt.net:443,37.3986,-121.964 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +nostrride.io,37.3986,-121.964 +nr.yay.so,46.2126,6.1154 +wheat.happytavern.co,43.6532,-79.3832 +qr.lostcause.nohost.me,-33.784,151.121 +nostr.bitcoiner.social,47.6743,-117.112 +relay.cypherflow.ai,48.8575,2.35138 +relay1.privkey.io,25.7617,-80.1918 +relay.sector01.com,41.4513,-81.7021 +strfry.apps3.slidestr.net,40.4167,-3.70329 +relay.froth.zone,60.1699,24.9384 +nostr-verified.wellorder.net,45.5201,-122.99 +relay.islandbitcoin.com,20.3898,78.0903 +n.musicroadmap.com,35.694,139.754 +relay.gulugulu.moe:443,43.6532,-79.3832 +fanfares.nostr1.com,40.7057,-74.0136 +nostr.carroarmato0.be:443,51.0355,4.48285 +conduitl2.fly.dev,38.7946,-106.535 +relay.hivetalk.org,40.8302,-74.1299 +nostr.hoppe-relay.it.com,42.8864,-78.8784 +qr.3nostr.com,42.8864,-78.8784 +strfry.shock.network,39.0438,-77.4874 +nostr.yutakobayashi.com,43.6532,-79.3832 +relay.plebeian.market,50.1109,8.68213 +auth.nostr1.com,40.7057,-74.0136 +nostr-relay.cbrx.io,43.6532,-79.3832 +relay.lanavault.space,60.1699,24.9384 +nostr.girino.org,43.6532,-79.3832 +espelho.girino.org,43.6532,-79.3832 +bendernostur.duckdns.org:8443,50.1109,8.68213 +nostr.whitenode45.ddns.net,40.55,-74.4758 +relay.scuba323.com,40.8218,-74.45 +relay.chatbett.de,40.7128,-74.006 +relay.decentralia.fr,48.122,11.589 +relay.nostx.io,43.6532,-79.3832 +relay.mypathtofire.de,42.8864,-78.8784 +relay.wasabiwallet.io,43.6532,-79.3832 +tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 +dev-relay.nostreon.com,60.1699,24.9384 +relay.directsponsor.net,42.8864,-78.8784 +nostr.4rs.nl,49.0291,8.35696 +nostr.tbxnetworx.de,47.8267,10.0205 +nip85.nosfabrica.com,39.0997,-94.5786 +relayrs.notoshi.win:443,43.6532,-79.3832 +nostr.christiansass.de,50.1109,8.68213 +nostr-pub.wellorder.net,45.5201,-122.99 +relay.fountain.fm:443,43.6532,-79.3832 +nostr.txlyre.website,60.1699,24.9384 +bitcoiner.social,47.6743,-117.112 +relay.wavlake.com,41.2619,-95.8608 +relay.internationalright-wing.org,-22.4692,-48.9875 +relay.samt.st,40.8302,-74.1299 +nostr.overmind.lol:443,43.6532,-79.3832 +relay2.orangesync.tech,40.7128,-74.006 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +relay.lightning.pub,39.0438,-77.4874 +schnorr.me,43.6532,-79.3832 +nostr.thalheim.io,60.1699,24.9384 +nostr.21crypto.ch,47.5356,8.73209 +openrelay.ziomc.com,45.4642,9.18998 +relay.underorion.se,50.1109,8.68213 +nostr-relay.zimage.com,34.282,-118.439 +relay.orangesync.tech,40.7128,-74.006 +nostr.n7ekb.net,47.4941,-122.294 +relay.kilombino.com,43.6532,-79.3832 +relay.s-w.art,43.6532,-79.3832 +relay.mleku.dev,32.7767,-96.797 +nostr.myshosholoza.co.za,52.3676,4.90414 +nostr.fullstackcash.net,45.5201,-122.99 +nostr.na.social,43.6532,-79.3832 +nostr.chaima.info,51.5072,-0.127586 +groups.beginningend.com,35.2227,-97.4786 +relay.zone667.com,60.1699,24.9384 +nostr.spicyz.io,43.6532,-79.3832 +nostr.islandarea.net:443,35.4669,-97.6473 +no.str.cr,8.96171,-83.5246 +nostr.wild-vibes.ts.net,48.8566,2.35222 +relay.veganostr.com,60.1699,24.9384 +relay.btcforplebs.com,43.6532,-79.3832 +relay.fountain.fm,43.6532,-79.3832 +nostr.linky.fit,50.1109,8.68213 +relay.nostr.dev.br,48.1046,11.6002 +relay.agentry.com,42.8864,-78.8784 +nostr.wecsats.io:443,43.6532,-79.3832 +syb.lol,34.0549,-118.243 +myvoiceourstory.org,37.3598,-121.981 +relay.sigit.io,50.4754,12.3683 +relay.bornheimer.app,50.1109,8.68213 +nostr.data.haus:443,50.4754,12.3683 +talon.quest,43.6532,-79.3832 +fanfares.nostr1.com:443,40.7057,-74.0136 +chorus.mikedilger.com:444,-36.8906,174.794 +relay.nexterz.com,43.6532,-79.3832 +relay.aarpia.com,37.3986,-121.964 +nostr.mikoshi.de,50.1109,8.68213 +relay.wisp.talk,49.4543,11.0746 +relay.jmoose.rocks,32.8009,-96.8195 +relay.vrtmrz.net:443,43.6532,-79.3832 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +relay.satsmarkt.club,52.6907,4.8181 +relay.gulugulu.moe,43.6532,-79.3832 +nostr.stakey.net,52.3676,4.90414 +relay.bullishbounty.com,43.6532,-79.3832 +nostr.debate.report,50.1109,8.68213 +node.kommonzenze.de,49.4521,11.0767 +relay.chorus.community,50.7529,6.84585 +nostr-relay.nextblockvending.com,47.2343,-119.853 +relayone.soundhsa.com,39.0997,-94.5786 +bcast.girino.org,43.6532,-79.3832 +bitsat.molonlabe.holdings,51.4012,-1.3147 +relay.nostr.com,50.1109,8.68213 +purplerelay.com,43.6532,-79.3832 +relay.plebeian.market:443,50.1109,8.68213 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +offchain.pub,39.1585,-94.5728 +relay.cosmicbolt.net,37.3986,-121.964 +nostr.rtvslawenia.com,49.4543,11.0746 +relay.nostrops.com,32.71,-96.6745 +nos.lol,50.4754,12.3683 +relay-na1.metanomalist.com,43.6532,-79.3832 +relay.primal.net,43.6532,-79.3832 +nostr.davenov.com,50.1109,8.68213 +nostr.nodesmap.com,59.3327,18.0656 +nostr.bitcoiner.social:443,47.6743,-117.112 +familiamartins.net.br,-22.8269,-43.0539 +dm-test-strfry-generic.samt.st,43.6532,-79.3832 +nostr.tagomago.me,42.3601,-71.0589 +relay.mitchelltribe.com:443,39.0438,-77.4874 +nostr.tabordalab.com,60.1699,24.9384 +relay.staging.plebeian.market:443,51.5072,-0.127586 +relay.orly.dev,32.7767,-96.797 +relay.pyramid.li,47.4093,8.46503 +relay.dyne.org,49.0291,8.35705 +nostr.thalheim.io:443,60.1699,24.9384 +relay.routstr.com,59.4016,17.9455 +wot.sudocarlos.com,43.7696,-79.3309 +relay.dreamith.to,43.6532,-79.3832 +relay.opmaat.org,60.1699,24.9384 +relay.ezernode.net,52.6921,6.19372 +relay.sigit.io:443,50.4754,12.3683 +relay.minibolt.info,43.6532,-79.3832 +relay.trotters.cc:443,43.6532,-79.3832 +nostr-relay.corb.net:443,39.6478,-104.988 +relay2.fiatdenier.com,50.1013,8.62643 +relay.openspecs.uid.ovh,50.9871,2.12554 +nostrcity-club.fly.dev,38.7946,-106.535 +nostr.myshosholoza.co.za:443,52.3676,4.90414 +strfry.shock.network:443,39.0438,-77.4874 +relay.nearhood.co.uk,51.5134,-0.0890675 +offchain.pub:443,39.1585,-94.5728 +cs-relay.nostrdev.com,50.4754,12.3683 +offchain.bostr.online,43.6532,-79.3832 +relay.bullishbounty.com:443,43.6532,-79.3832 +relay.wisp.talk:443,49.4543,11.0746 +relay2.veganostr.com,60.1699,24.9384 +relay.mappingbitcoin.com,43.6532,-79.3832 +nostr.liberty.fans,36.8767,-89.5879 +kotukonostr.onrender.com,37.7775,-122.397 +relay-testnet.k8s.layer3.news,37.3387,-121.885 +nostr.hifish.org,47.4009,8.57814 +nostr.stakey.net:443,52.3676,4.90414 +relay.mccormick.cx,52.3563,4.95714 +relay.paulstephenborile.com:443,49.4543,11.0746 +relay-us.zombi.cloudrodion.com,40.7862,-74.0743 +nostrelay.circum.space:443,52.2245,8.826 +relay.nostriot.com,41.5695,-83.9786 +schnorr.me:443,43.6532,-79.3832 +nostr.bond,50.1109,8.68213 +temp.iris.to,43.6532,-79.3832 +cache.trustr.ing,43.6548,-79.3885 +relay.snotr.nl:49999,51.9758,4.31389 +relay.bitos.space,43.6532,-79.3832 +nostr.azzamo.net,52.2633,21.0283 +relay.dwadziesciajeden.pl,52.2297,21.0122 +relay.tdw.gg,34.0549,-118.243 +relay.44billion.net,43.6532,-79.3832 +maxq.descendant.io,43.6532,-79.3832 +public.crostr.com:443,43.6532,-79.3832 +nostr.easycryptosend.it,43.6532,-79.3832 +relay.ltgnet.work:8443,51.0511,-114.075 +nostr2.girino.org:443,43.6532,-79.3832 +ynostr.yael.at,60.1699,24.9384 +nostr.azzamo.net:443,52.2633,21.0283 +nrs-01.darkcloudarcade.com,39.0997,-94.5786 +relay.sharegap.net,43.6532,-79.3832 +relay.guggero.org,46.5971,9.59652 +relay.pprgb.app,52.52,13.405 +relay.jmoose.rocks:443,32.8009,-96.8195 +relay.klabo.world,47.674,-122.122 +nostr.chaima.info:443,51.5072,-0.127586 +nostr-02.uid.ovh,50.9871,2.12554 +nostr.tac.lol,47.4748,-122.273 +relay.nostrian-conquest.com,41.223,-111.974 +hasky.chat,40.7862,-74.0743 +relay.satmaxt.xyz,43.6532,-79.3832 +bridge.tagomago.me,42.3601,-71.0589 +treuzkas.branruz.com,48.8575,2.35138 +relay.illuminodes.com,43.6532,-79.3832 +relay.damus.io,43.6532,-79.3832 +nostr.twinkle.lol,51.902,7.6657 +relay.getsafebox.app:443,43.6532,-79.3832 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +nostr.sovereignservices.xyz,43.6532,-79.3832 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +vm-1734.lnvps.cloud,53.3498,-6.26031 +public.crostr.com,43.6532,-79.3832 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +nostr.emanuelemiani.it,45.778,8.79414 +relay.ru.ac.th,13.7607,100.627 +relay.piazza.today,48.122,11.589 +support.flotilla.social,32.9483,-96.7299 +nostr-01.uid.ovh,50.9871,2.12554 +freelay.sovbit.host,60.1699,24.9384 +nostr.data.haus,50.4754,12.3683 +nostr-01.yakihonne.com:443,1.32123,103.695 +relay.wavlake.com:443,41.2619,-95.8608 +no.str.cr:443,8.96171,-83.5246 +dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 +relay.bowlafterbowl.com,32.9483,-96.7299 +strfry.bonsai.com:443,39.0438,-77.4874 +nostr.2b9t.xyz,34.0549,-118.243 +rele.speyhard.fi,51.5072,-0.127586 +relay.lacrypta.ar,43.6532,-79.3832 +infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 +reraw.pbla2fish.cc,43.6532,-79.3832 +relay.nostr.space,43.6532,-79.3832 +relay.atsocy.com,43.6532,-79.3832 +nostr-relay.xbytez.io:443,50.6924,3.20113 +relay.earthly.city,34.1749,-118.54 +relay.noeudlibre.fr,50.6924,3.20113 +inbox.scuba323.com,40.8218,-74.45 +relay.getsafebox.app,43.6532,-79.3832 +relay.cyberguy.fyi,52.6907,4.8181 +bitchat.nostr1.com,40.7057,-74.0136 +nostrelay.circum.space,52.2245,8.826 +nostr.oxtr.dev:443,50.4754,12.3683 +relay.degmods.com,50.4754,12.3683 +relay.ohstr.com,43.6532,-79.3832 +relay.bnos.space,43.6532,-79.3832 +relay.novospes.com,43.6532,-79.3832 +relay.satsapp.me,50.4754,12.3683 +nostr.sathoarder.com:443,48.5734,7.75211 +nostr-01.yakihonne.com,1.32123,103.695 +relay.trotters.cc,43.6532,-79.3832 +relay.ditto.pub,43.6532,-79.3832 +relay.angor.io,48.1046,11.6002 +nostr.mom,50.4754,12.3683 +nostr.vulpem.com,49.4543,11.0746 nostr-dev.wellorder.net,45.5201,-122.99 nostr.notribe.net,40.8302,-74.1299 -relay.mleku.dev,32.7767,-96.797 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -nostr2.girino.org:443,43.6532,-79.3832 -relay.cosmicbolt.net:443,37.3986,-121.964 -ribo.us.nostria.app,43.6532,-79.3832 -nostr.janx.com,43.6532,-79.3832 -familiamartins.net.br,-22.8833,-43.1036 -relay.yoinekodo.jp,43.6532,-79.3832 -nostr.4rs.nl,49.0291,8.35696 -relay.routstr.com,59.4016,17.9455 -relay.bitmacro.cloud,43.6532,-79.3832 -relay.mappingbitcoin.com,43.6532,-79.3832 -nostr.dlcdevkit.com:443,40.0992,-83.1141 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -nostr.overmind.lol:443,43.6532,-79.3832 -relayone.geektank.ai:443,39.0997,-94.5786 -relay.staging.plebeian.market:443,51.5072,-0.127586 -relay.kilombino.com,43.6532,-79.3832 -relay.endfiat.money:443,59.3327,18.0656 -social.amanah.eblessing.co,48.1046,11.6002 -nostr.2b9t.xyz,34.0549,-118.243 -relay.sector01.com,41.4513,-81.7021 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -relay.nostr.dev.br,48.1046,11.6002 -x.kojira.io,43.6532,-79.3832 -node.kommonzenze.de,49.4521,11.0767 -relay.earthly.city,34.1749,-118.54 -buzz.cashu.space,50.1109,8.68213 -relay.kizuna-miki.com,35.694,139.754 -bitchat.nostr1.com,40.7057,-74.0136 -bruh.samt.st,43.6532,-79.3832 -relay2.fiatdenier.com,50.1013,8.62643 -nostr.oxtr.dev,50.4754,12.3683 -relay.pocketstr.com,40.8302,-74.1299 -nostr.thalheim.io,60.1699,24.9384 -relay.qstr.app,50.1109,8.68213 -ynostr.yael.at,60.1699,24.9384 -relay.zone667.com,60.1699,24.9384 -relay.underorion.se,50.1109,8.68213 -relay.beamhop.com,43.6532,-79.3832 -relay.paulstephenborile.com,49.4543,11.0746 -nostr.easycryptosend.it,43.6532,-79.3832 -nostr-verified.wellorder.net,45.5201,-122.99 -nostr.hekster.org:443,37.3986,-121.964 -relay.samt.st,40.8302,-74.1299 -nostr.islandarea.net:443,35.4669,-97.6473 -nostr-01.uid.ovh,50.9871,2.12554 -relay-dev.gulugulu.moe,43.6532,-79.3832 -relay.44billion.net,43.6532,-79.3832 -dev-relay.nostreon.com,60.1699,24.9384 -nostr.pbfs.io,50.4754,12.3683 -nrs-01.darkcloudarcade.com,39.0997,-94.5786 -relay01.lnfi.network,35.6764,139.65 -relay.mostr.pub,43.6532,-79.3832 -strfry.apps3.slidestr.net,40.4167,-3.70329 -nostr.linky.fit,50.1109,8.68213 -nostr.davenov.com,50.1109,8.68213 -relay.noeudlibre.fr,50.6924,3.20113 -hasky.chat,40.7862,-74.0743 -relay.artio.inf.unibe.ch,46.9501,7.43678 -relay.manneken.brussels,49.4543,11.0746 -relay.veganostr.com,60.1699,24.9384 -relay.nexterz.com,43.6532,-79.3832 -nostr.snowbla.de,50.4754,12.3683 -nostr.unkn0wn.world,46.8499,9.53287 -nostr.wild-vibes.ts.net,48.8566,2.35222 -relay.bitos.space,43.6532,-79.3832 -relay-na1.metanomalist.com,43.6532,-79.3832 -relay.favillakey.io,-27.4705,153.026 -relay.nearhood.co.uk,51.5134,-0.0890675 -relay.illuminodes.com,43.6532,-79.3832 -nostr.dlcdevkit.com,40.0992,-83.1141 -relay.nostr.net,43.6532,-79.3832 -relay.trotters.cc:443,43.6532,-79.3832 -cyberspace.nostr1.com,40.7057,-74.0136 -nostr-relay.nextblockvending.com,47.2343,-119.853 -relay.btcforplebs.com,43.6532,-79.3832 -nostr.azzamo.net:443,52.2633,21.0283 -relay.tdw.gg,34.0549,-118.243 -relay.veganostr.com:443,60.1699,24.9384 -wisp.djorivaltech.com,43.6532,-79.3832 -relay.cosmicbolt.net,37.3986,-121.964 -nostr.debate.report,50.1109,8.68213 -nostr.na.social,43.6532,-79.3832 -relay.kaleidoswap.com,50.8476,4.35717 -nostr.sathoarder.com:443,48.5734,7.75211 -relay.getsafebox.app:443,43.6532,-79.3832 -relay.aarpia.com,37.3986,-121.964 -relay.satsapp.me,50.4754,12.3683 -relay.decentralia.fr,48.122,11.589 -relay.beginningend.com,35.2227,-97.4786 -relay.paulstephenborile.com:443,49.4543,11.0746 -relay.satmaxt.xyz,43.6532,-79.3832 -nostr.oxtr.dev:443,50.4754,12.3683 -vault.iris.to,43.6532,-79.3832 -relay.ditto.pub:443,43.6532,-79.3832 -relay.atsocy.com,43.6532,-79.3832 -relay.vrtmrz.net,43.6532,-79.3832 -soloco.nl,43.6532,-79.3832 -nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 -relayone.soundhsa.com:443,39.0997,-94.5786 -nostr.wawasoft.net,43.6532,-79.3832 -relay.solife.me,43.6532,-79.3832 -nostr.mom,50.4754,12.3683 -relay.getvia.xyz,60.1699,24.9384 -freelay.sovbit.host,60.1699,24.9384 -relay-rpi.edufeed.org:443,49.4521,11.0767 -relay.bullishbounty.com:443,43.6532,-79.3832 -schnorr.me:443,43.6532,-79.3832 -temp.iris.to,43.6532,-79.3832 -nostr-relay.cbrx.io,43.6532,-79.3832 -strfry.bonsai.com:443,39.0438,-77.4874 -nostr.hoppe-relay.it.com,42.8864,-78.8784 -dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -relay.nostriches.club,43.6532,-79.3832 -relay.dyne.org,49.0291,8.35705 -yabu.me,35.6092,139.73 -relay.chorus.community:443,48.5333,10.7 -cs-relay.nostrdev.com:443,50.4754,12.3683 -relay.mwaters.net,50.9871,2.12554 -no.str.cr,8.96171,-83.5246 -nostr.hifish.org,47.4009,8.57814 -relay.directsponsor.net,42.8864,-78.8784 public.obelisk.ar,43.6532,-79.3832 -nostr.carroarmato0.be,50.914,3.21378 -nostr.sovereignservices.xyz,43.6532,-79.3832 -nostr.pbfs.io:443,50.4754,12.3683 -relay.degmods.com,50.4754,12.3683 -relay.plebeian.market,50.1109,8.68213 -offchain.pub,39.1585,-94.5728 -nostr.thebiglake.org,32.71,-96.6745 -bendernostur.duckdns.org:8443,50.1109,8.68213 -nostr-relay.zimage.com,34.282,-118.439 -nostr.ac,38.958,-77.3592 -relay.nostr.com,50.1109,8.68213 -bitcoiner.social,47.6743,-117.112 -nostr.wecsats.io,43.6532,-79.3832 -relay.pprgb.app,52.52,13.405 -nostr.girino.org,43.6532,-79.3832 -nostr.bitcoiner.social,47.6743,-117.112 -nostr.planix.org,43.6532,-79.3832 -nostr.data.haus:443,50.4754,12.3683 -relay.nostrmap.net:443,60.1699,24.9384 -relay.arx-ccn.com,50.4754,12.3683 -relay.agorist.space:443,52.3734,4.89406 -relay.laantungir.net,-19.4692,-42.5315 -relay.edufeed.org:443,49.4521,11.0767 -nostrcity-club.fly.dev:443,38.7946,-106.535 -nostr.nodesmap.com,59.3327,18.0656 +relay.mostr.pub,43.6532,-79.3832 From 08974107dc67de4ce6528bb7305d412d52a1c726 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:20:35 +0300 Subject: [PATCH 41/45] Compact Wear chat headers and stabilize scroll controls --- .../java/com/bitchat/watch/ui/ChatScaffold.kt | 129 ++++----- .../java/com/bitchat/watch/ui/ChatScreen.kt | 6 +- .../java/com/bitchat/watch/ui/DmScreen.kt | 4 +- .../com/bitchat/watch/ui/WearChatHeader.kt | 24 +- .../bitchat/watch/ui/ChatAutoScrollTest.kt | 252 ++++++++++-------- .../watch/ui/WearDisplayGeometryTest.kt | 16 +- 6 files changed, 242 insertions(+), 189 deletions(-) diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt index 20880cab..ffd08ead 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScaffold.kt @@ -28,14 +28,19 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState import androidx.wear.compose.foundation.lazy.items @@ -50,13 +55,12 @@ import com.bitchat.watch.ui.theme.ChatVisualTokens import com.bitchat.watch.ui.theme.LocalBitchatPalette import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlin.math.sign /** * The shared chat body for global chat and DM threads, following the classic messenger * pattern: a TransformingLazyColumn message list (native Wear center-scaling/fade, rotary, * scrollbar) with the header and action bar as floating overlays that get out of the way - * while scrolling up into history and return on any downward scroll; at the newest message + * while scrolling up into history and return on a deliberate reverse scroll; at the newest message * they are always visible. * * The list's contentPadding is CONSTANT and both overlays are layout-neutral, so showing or @@ -92,33 +96,38 @@ fun ChatScaffold( // Follow intent is changed only by an actual user scroll away from the newest item or by // reaching the end again. A new item temporarily makes canScrollForward true before layout; // treating that transient range change as user intent breaks automatic following. - var followNewest by remember { mutableStateOf(true) } - val controlsVisible = remember { mutableStateOf(true) } + var scrollIntent by remember { mutableStateOf(ChatScrollIntentState()) } + val density = LocalDensity.current + val scrollConnection = remember(columnState, density) { + object : NestedScrollConnection { + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource + ): Offset { + // Both Wear rotary and touch dispatch consumed movement here. Positive list + // movement is toward newer messages. Layout changes never enter this path. + scrollIntent = updatedChatScrollIntent( + current = scrollIntent, + deltaDp = -consumed.y / density.density, + isUserInput = source == NestedScrollSource.UserInput, + atNewest = !columnState.canScrollForward + ) + return Offset.Zero + } + } + } LaunchedEffect(columnState) { - var lastPosition = -1 - var scrollIntent = ChatScrollIntentState() snapshotFlow { - val first = columnState.layoutInfo.visibleItems.firstOrNull() - ChatScrollSnapshot( - canScrollForward = columnState.canScrollForward, - isScrollInProgress = columnState.isScrollInProgress, - position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0) - ) - }.collect { snapshot -> - scrollIntent = updatedChatScrollIntent( - current = scrollIntent, - snapshot = snapshot, - previousPosition = lastPosition - ) - followNewest = scrollIntent.followsNewest - controlsVisible.value = scrollIntent.controlsVisible - lastPosition = snapshot.position + !columnState.canScrollForward && !columnState.isScrollInProgress + }.collect { atNewest -> + if (atNewest) scrollIntent = ChatScrollIntentState() } } // Stick to bottom when the user has not intentionally moved into history. LaunchedEffect(columnState, messages.size) { - if (messages.isNotEmpty() && followNewest) { + if (messages.isNotEmpty() && scrollIntent.followsNewest) { val expectedSingleMessageKey = messages.singleOrNull()?.id scrollToNewestAfterItemsMeasured( expectedItemCount = messages.size, @@ -137,7 +146,14 @@ fun ChatScaffold( ) { // scrollBy to the end of the range: animateScrollToItem stops as soon as the // item is partially visible, which left the last message cropped. - columnState.scroll { scrollBy(Float.MAX_VALUE) } + // Do not seize the list from an active drag/crown gesture, or follow an + // append whose measurement completed after the user entered history. + followNewestWhenIdle( + scrolling = snapshotFlow { columnState.isScrollInProgress }, + shouldFollow = { scrollIntent.followsNewest } + ) { + columnState.scroll { scrollBy(Float.MAX_VALUE) } + } } } } @@ -149,10 +165,10 @@ fun ChatScaffold( voice = voice, onOpenImage = onOpenImage, columnState = columnState, - controlsVisible = controlsVisible.value, + controlsVisible = scrollIntent.controlsVisible, header = header, actionBar = actionBar, - modifier = Modifier.fillMaxSize() + modifier = Modifier.fillMaxSize().nestedScroll(scrollConnection) ) } @@ -161,47 +177,30 @@ internal data class MeasuredChatLayout( val singleVisibleItemKey: Any? ) -internal data class ChatScrollSnapshot( - val canScrollForward: Boolean, - val isScrollInProgress: Boolean, - val position: Int -) - internal data class ChatScrollIntentState( val followsNewest: Boolean = true, val controlsVisible: Boolean = true, - val accumulatedDeltaPx: Int = 0 + val reversalDp: Float = 0f ) internal fun updatedChatScrollIntent( current: ChatScrollIntentState, - snapshot: ChatScrollSnapshot, - previousPosition: Int + deltaDp: Float, + isUserInput: Boolean, + atNewest: Boolean ): ChatScrollIntentState { - if (!snapshot.canScrollForward) return ChatScrollIntentState() - if (!snapshot.isScrollInProgress || previousPosition < 0) return current - - val delta = snapshot.position - previousPosition - val accumulatedDelta = when { - delta == 0 -> current.accumulatedDeltaPx - current.accumulatedDeltaPx == 0 || - current.accumulatedDeltaPx.sign == delta.sign -> - current.accumulatedDeltaPx + delta - else -> delta - } - val movedAway = accumulatedDelta <= -CHAT_SCROLL_DIRECTION_THRESHOLD_PX - val movedTowardNewest = accumulatedDelta >= CHAT_SCROLL_DIRECTION_THRESHOLD_PX - + if (atNewest) return ChatScrollIntentState() + if (!isUserInput || !deltaDp.isFinite() || deltaDp == 0f) return current + // Hysteresis measures net travel opposite the current controls state, not the sum of + // tiny back-and-forth movements. Keep it across discrete crown ticks and idle periods. + val reversal = (current.reversalDp + if (current.controlsVisible) -deltaDp else deltaDp) + .coerceAtLeast(0f) + val threshold = if (current.controlsVisible) 12f else 24f + val toggle = reversal >= threshold return current.copy( - followsNewest = current.followsNewest && !movedAway, - controlsVisible = when { - movedAway -> false - movedTowardNewest -> true - else -> current.controlsVisible - }, - // Keep sub-threshold movement across discrete rotary events. Once intent is clear, - // start a fresh accumulator so reversing direction gets the same threshold treatment. - accumulatedDeltaPx = if (movedAway || movedTowardNewest) 0 else accumulatedDelta + followsNewest = current.followsNewest && deltaDp >= 0f, + controlsVisible = if (toggle) !current.controlsVisible else current.controlsVisible, + reversalDp = if (toggle) 0f else reversal ) } @@ -219,6 +218,15 @@ internal suspend fun scrollToNewestAfterItemsMeasured( scrollToEnd() } +internal suspend fun followNewestWhenIdle( + scrolling: Flow, + shouldFollow: () -> Boolean, + scrollToEnd: suspend () -> Unit +) { + scrolling.first { !it } + if (shouldFollow()) scrollToEnd() +} + @Composable private fun ChatBody( messages: List, @@ -236,6 +244,9 @@ private fun ChatBody( val context = LocalContext.current val transformationSpec = rememberTransformationSpec() val isScreenRound = LocalConfiguration.current.isScreenRound + val headerClearance = with(LocalDensity.current) { + maxOf(40.dp, 24.dp + (14f * 1.3f).sp.toDp() / 2 + 6.dp) + } // Slide-to-cancel: while recording, the finger's position is tracked globally; the // overlay's mic button reports its bounds and becomes the cancel target when the // finger hovers it (with generous slack so the snap engages on approach). @@ -362,7 +373,7 @@ private fun ChatBody( // duplicated padding and a shortened list viewport. contentPadding = scaffoldPadding.withVerticalClearance( layoutDirection = layoutDirection, - top = CHAT_HEADER_CONTENT_CLEARANCE, + top = headerClearance, bottom = CHAT_ACTION_BAR_CLEARANCE ) ) { @@ -437,8 +448,6 @@ private fun ChatBody( // Extra finger slack (px, ~28dp at watch density) around the cancel target so the snap // engages as the finger approaches, not only on exact contact. private const val CANCEL_HOVER_SLANT_PX = 56f -private const val CHAT_SCROLL_DIRECTION_THRESHOLD_PX = 24 -private val CHAT_HEADER_CONTENT_CLEARANCE = 56.dp private val CHAT_ACTION_BAR_CLEARANCE = 64.dp private val CHAT_HEADER_EDGE_FADE = 36.dp private val CHAT_ACTION_BAR_EDGE_FADE = 72.dp diff --git a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt index d3f275e3..d576cd03 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/ChatScreen.kt @@ -127,13 +127,13 @@ private fun ChatHeader( ) val iconSize by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 16.dp else 11.dp, + targetValue = if (expanded) 14.dp else 12.dp, animationSpec = spec, label = "hdrIcon", ) val titleSize by androidx.compose.animation.core.animateFloatAsState( - targetValue = if (expanded) 15f else 12f, + targetValue = if (expanded) 14f else 12f, animationSpec = androidx.compose.animation.core.tween(BitchatMotion.STANDARD_MS), label = "hdrTitle", ) @@ -156,7 +156,7 @@ private fun ChatHeader( color = MaterialTheme.colorScheme.primary, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false).padding(end = 8.dp), + modifier = Modifier.weight(1f, fill = false).padding(end = 2.dp), ) } Icon( diff --git a/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt b/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt index eac4a5c9..d0e675ea 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/DmScreen.kt @@ -150,13 +150,13 @@ private fun DmHeader( ) val headerIconSize by androidx.compose.animation.core.animateDpAsState( - targetValue = if (expanded) 16.dp else 11.dp, + targetValue = if (expanded) 14.dp else 12.dp, animationSpec = spec, label = "dmHdrIcon", ) val headerTitleSize by androidx.compose.animation.core.animateFloatAsState( - targetValue = if (expanded) 15f else 12f, + targetValue = if (expanded) 14f else 12f, animationSpec = androidx.compose.animation.core.tween(BitchatMotion.STANDARD_MS), label = "dmHdrTitle", ) diff --git a/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt b/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt index b30ab885..aeb19766 100644 --- a/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt +++ b/wear/src/main/java/com/bitchat/watch/ui/WearChatHeader.kt @@ -1,6 +1,5 @@ package com.bitchat.watch.ui -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints @@ -8,11 +7,11 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration @@ -33,16 +32,22 @@ internal fun WearChatHeader( val configuration = LocalConfiguration.current val lineHeight = with(LocalDensity.current) { (fontSize * 1.3f).sp.toDp() } val rowHeight = maxOf(48.dp, lineHeight) - val top = 12.dp + (rowHeight - lineHeight) / 2 + val top = (rowHeight - lineHeight) / 2 + val background = MaterialTheme.colorScheme.background + // Follow the existing title animation without animating layout or the hit target. + val expansion = ((fontSize - 12f) / 2f).coerceIn(0f, 1f) + val fadeEnd = maxOf((36f + 8f * expansion).dp, top + lineHeight + 4.dp) BoxWithConstraints( - Modifier.fillMaxWidth() - .background( + Modifier.fillMaxWidth().drawWithCache { + val brush = Brush.verticalGradient( - 0f to MaterialTheme.colorScheme.background, - 0.75f to MaterialTheme.colorScheme.background, + 0f to background, + 0.65f to background.copy(alpha = 0.95f), 1f to Color.Transparent, + endY = fadeEnd.toPx(), ) - ), + onDrawBehind { drawRect(brush) } + }, contentAlignment = Alignment.TopCenter, ) { val safeWidth = @@ -59,8 +64,7 @@ internal fun WearChatHeader( } Row( modifier = - Modifier.padding(top = 12.dp) - .width(safeWidth.coerceAtLeast(0.dp)) + Modifier.width(safeWidth.coerceAtLeast(48.dp)) .height(rowHeight) .clickable(role = Role.Button, onClickLabel = onClickLabel, onClick = onClick), horizontalArrangement = Arrangement.Center, diff --git a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt index 7b2d13b9..79242d85 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/ChatAutoScrollTest.kt @@ -11,99 +11,131 @@ import org.junit.Test class ChatAutoScrollTest { @Test - fun `new scroll range from appended message keeps follow intent`() { - val updated = updatedChatScrollIntent( - current = ChatScrollIntentState(), - snapshot = ChatScrollSnapshot( - canScrollForward = true, - isScrollInProgress = false, - position = 100 - ), - previousPosition = 100 - ) - - assertEquals(ChatScrollIntentState(), updated) + fun `append waits for active gesture and rechecks history intent`() = runTest { + val scrolling = MutableStateFlow(true) + var followsNewest = true + var scrollCount = 0 + val job = + launch(start = CoroutineStart.UNDISPATCHED) { + followNewestWhenIdle(scrolling, { followsNewest }) { scrollCount++ } + } + assertFalse(job.isCompleted) + assertEquals(0, scrollCount) + followsNewest = false + scrolling.value = false + job.join() + assertEquals(0, scrollCount) } @Test - fun `user scroll away disables follow until list reaches newest again`() { - val browsingHistory = updatedChatScrollIntent( - current = ChatScrollIntentState(), - snapshot = ChatScrollSnapshot( - canScrollForward = true, - isScrollInProgress = true, - position = 60 - ), - previousPosition = 100 - ) - assertFalse(browsingHistory.followsNewest) - assertFalse(browsingHistory.controlsVisible) + fun `append follows after gesture settles when user remains at newest`() = runTest { + val scrolling = MutableStateFlow(true) + var scrollCount = 0 + val job = + launch(start = CoroutineStart.UNDISPATCHED) { + followNewestWhenIdle(scrolling, { true }) { scrollCount++ } + } + assertEquals(0, scrollCount) + scrolling.value = false + job.join() + assertEquals(1, scrollCount) + } - val dockedAgain = updatedChatScrollIntent( - current = browsingHistory, - snapshot = ChatScrollSnapshot( - canScrollForward = false, - isScrollInProgress = false, - position = 200 - ), - previousPosition = 60 - ) - assertEquals(ChatScrollIntentState(), dockedAgain) + private fun move( + state: ChatScrollIntentState, + dp: Float, + user: Boolean = true, + newest: Boolean = false, + ) = updatedChatScrollIntent(state, dp, user, newest) + + @Test + fun `append and programmatic movement never change intent`() { + val docked = ChatScrollIntentState() + assertEquals(docked, move(docked, 1000f, user = false)) + val history = ChatScrollIntentState(false, false) + assertEquals(history, move(history, 1000f, user = false)) } @Test - fun `slow scroll away accumulates intent across sub-threshold updates`() { - var state = ChatScrollIntentState() - var previousPosition = 100 - - listOf(94, 88, 82, 76).forEach { position -> - state = updatedChatScrollIntent( - current = state, - snapshot = ChatScrollSnapshot( - canScrollForward = true, - isScrollInProgress = true, - position = position - ), - previousPosition = previousPosition - ) - previousPosition = position - state = updatedChatScrollIntent( - current = state, - snapshot = ChatScrollSnapshot( - canScrollForward = true, - isScrollInProgress = false, - position = position - ), - previousPosition = previousPosition - ) - } - - assertFalse(state.followsNewest) - assertFalse(state.controlsVisible) - assertEquals(0, state.accumulatedDeltaPx) - } - - @Test - fun `slow scroll toward newest reveals controls without restoring follow early`() { - var state = ChatScrollIntentState(followsNewest = false, controlsVisible = false) - var previousPosition = 60 - - listOf(66, 72, 78, 84).forEach { position -> - state = updatedChatScrollIntent( - current = state, - snapshot = ChatScrollSnapshot( - canScrollForward = true, - isScrollInProgress = true, - position = position - ), - previousPosition = previousPosition - ) - previousPosition = position - } - + fun `first consumed movement away suspends following before controls hide`() { + val state = move(ChatScrollIntentState(), -1f) assertFalse(state.followsNewest) assertEquals(true, state.controlsVisible) - assertEquals(0, state.accumulatedDeltaPx) + assertFalse(move(state, -11f).controlsVisible) + } + + @Test + fun `deliberate reversal reveals controls but does not resume following`() { + val history = move(ChatScrollIntentState(), -12f) + val almost = move(history, 23f) + assertFalse(almost.controlsVisible) + val revealed = move(almost, 1f) + assertEquals(true, revealed.controlsVisible) + assertFalse(revealed.followsNewest) + assertEquals(ChatScrollIntentState(), move(revealed, 1f, newest = true)) + } + + @Test + fun `jitter does not accumulate into repeated toggles`() { + var state = move(ChatScrollIntentState(), -12f) + repeat(100) { + state = move(state, 3f) + state = move(state, -3f) + } + assertFalse(state.controlsVisible) + assertEquals(0f, state.reversalDp) + } + + @Test + fun `pauses and discrete crown ticks retain net movement`() { + var state = ChatScrollIntentState() + repeat(4) { + state = move(state, -3f) + state = move(state, 0f, user = false) + } + assertFalse(state.controlsVisible) + repeat(8) { + state = move(state, 3f) + state = move(state, 0f, user = false) + } + assertEquals(true, state.controlsVisible) + assertFalse(state.followsNewest) + } + + @Test + fun `fling preserves controls until newest is reached`() { + val history = move(ChatScrollIntentState(), -12f) + assertEquals(history, move(history, 1000f, user = false)) + assertEquals(history, move(history, -1000f, user = false)) + assertEquals(ChatScrollIntentState(), move(history, 1f, user = false, newest = true)) + } + + @Test + fun `consumed distance is independent of item boundaries and event chunking`() { + val initial = ChatScrollIntentState() + val oneEvent = move(initial, -12f) + val acrossRows = + listOf(-2f, -3f, -1f, -6f).fold(initial) { state, delta -> + move(state, delta) + } + assertEquals(oneEvent, acrossRows) + } + + @Test + fun `pixel distances normalize to the same dp thresholds`() { + for (density in listOf(1f, 1.6875f, 2f, 3f)) { + val history = move(ChatScrollIntentState(), (-12f * density) / density) + assertFalse(history.controlsVisible) + assertEquals(true, move(history, (24f * density) / density).controlsVisible) + } + } + + @Test + fun `invalid and unconsumed input is ignored`() { + val initial = ChatScrollIntentState() + assertEquals(initial, move(initial, Float.NaN)) + assertEquals(initial, move(initial, Float.POSITIVE_INFINITY)) + assertEquals(initial, move(initial, 0f)) } @Test @@ -111,15 +143,16 @@ class ChatAutoScrollTest { val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null)) var scrollCount = 0 - val scrollJob = launch(start = CoroutineStart.UNDISPATCHED) { - scrollToNewestAfterItemsMeasured( - expectedItemCount = 4, - expectedSingleMessageKey = null, - measuredLayouts = measuredLayouts - ) { - scrollCount += 1 + val scrollJob = + launch(start = CoroutineStart.UNDISPATCHED) { + scrollToNewestAfterItemsMeasured( + expectedItemCount = 4, + expectedSingleMessageKey = null, + measuredLayouts = measuredLayouts, + ) { + scrollCount += 1 + } } - } assertFalse(scrollJob.isCompleted) assertEquals(0, scrollCount) @@ -138,7 +171,7 @@ class ChatAutoScrollTest { scrollToNewestAfterItemsMeasured( expectedItemCount = 4, expectedSingleMessageKey = null, - measuredLayouts = measuredLayouts + measuredLayouts = measuredLayouts, ) { scrollCount += 1 } @@ -149,28 +182,31 @@ class ChatAutoScrollTest { @Test fun `first message waits past stale empty placeholder layout`() = runTest { val messageKey = "first-message" - val measuredLayouts = MutableStateFlow( - MeasuredChatLayout(itemCount = 1, singleVisibleItemKey = "empty-placeholder") - ) + val measuredLayouts = + MutableStateFlow( + MeasuredChatLayout(itemCount = 1, singleVisibleItemKey = "empty-placeholder") + ) var scrollCount = 0 - val scrollJob = launch(start = CoroutineStart.UNDISPATCHED) { - scrollToNewestAfterItemsMeasured( - expectedItemCount = 1, - expectedSingleMessageKey = messageKey, - measuredLayouts = measuredLayouts - ) { - scrollCount += 1 + val scrollJob = + launch(start = CoroutineStart.UNDISPATCHED) { + scrollToNewestAfterItemsMeasured( + expectedItemCount = 1, + expectedSingleMessageKey = messageKey, + measuredLayouts = measuredLayouts, + ) { + scrollCount += 1 + } } - } assertFalse(scrollJob.isCompleted) assertEquals(0, scrollCount) - measuredLayouts.value = MeasuredChatLayout( - itemCount = 1, - singleVisibleItemKey = messageKey - ) + measuredLayouts.value = + MeasuredChatLayout( + itemCount = 1, + singleVisibleItemKey = messageKey, + ) scrollJob.join() assertEquals(1, scrollCount) diff --git a/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt b/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt index 96bc4e99..c274657c 100644 --- a/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt +++ b/wear/src/test/java/com/bitchat/watch/ui/WearDisplayGeometryTest.kt @@ -11,12 +11,16 @@ class WearDisplayGeometryTest { fun `header band corners stay inside the circle at supported text sizes`() { for (diameter in listOf(192f, 228f, 240f)) { for (scale in listOf(0.94f, 1f, 1.24f, 1.3f)) { - val lineHeight = 15f * 1.3f * scale - val top = 12f + (maxOf(48f, lineHeight) - lineHeight) / 2f - val width = roundBandWidth(diameter, diameter, top, top + lineHeight) - val radius = diameter / 2f - for (y in listOf(top, top + lineHeight)) { - assertTrue((width / 2).pow(2) + (y - radius).pow(2) <= radius.pow(2) + 0.01f) + for (titleSize in listOf(12f, 13f, 14f)) { + val lineHeight = titleSize * 1.3f * scale + val top = (maxOf(48f, lineHeight) - lineHeight) / 2f + val width = roundBandWidth(diameter, diameter, top, top + lineHeight) + val radius = diameter / 2f + for (y in listOf(top, top + lineHeight)) { + assertTrue( + (width / 2).pow(2) + (y - radius).pow(2) <= radius.pow(2) + 0.01f + ) + } } } } From 0fce9faa1f7f24f256775fca73b04cf48f74af8d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:55:50 +0300 Subject: [PATCH 42/45] Bump Wear OS version to 0.1.3 --- wear/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts index 9bd87331..a4d37f68 100644 --- a/wear/build.gradle.kts +++ b/wear/build.gradle.kts @@ -19,8 +19,8 @@ android { targetSdk = libs.versions.targetSdk.get().toInt() // Wear releases use a separate high range because Play requires every artifact in // one application ID to have a unique version code across all form factors. - versionCode = 1_000_000_003 - versionName = "0.1.2" + versionCode = 1_000_000_004 + versionName = "0.1.3" vectorDrawables { useSupportLibrary = true From fa1727de2f9daebea1ce322653d2e326a6f40c34 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:29:23 +0300 Subject: [PATCH 43/45] docs: simplify packet deduplication comment --- .../bitchat/android/mesh/SecurityManager.kt | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 7efa7fb8..6fab9f03 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -239,29 +239,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private return encryptionService.getCombinedPublicKeyData() } - /** - * Generate message ID for duplicate detection - */ - /** - * Identity used for replay and duplicate detection. - * - * This was a 32-bit `contentHashCode()` over at most the first 64 bytes of - * the payload. Two packets from the same peer in the same millisecond that - * agreed on that prefix collided, and a collision here is a *dropped - * message* — the second packet is discarded as a duplicate and there is no - * signal that it happened. - * - * `PacketIdUtil` is the identity the rest of the stack already uses for - * exactly this question (gossip sync membership, message IDs in - * `MessageHandler`), and iOS derives it the same way: the first 16 bytes of - * SHA-256 over type, senderID, timestamp and the **whole** payload. Using - * it here makes the security path agree with the sync path instead of - * carrying a weaker private notion of "same packet". - * - * Peer scoping is kept: `PacketIdUtil` covers the packet's own senderID, - * while this key is scoped by the peer the packet was received from, and - * those are not the same thing for a relayed packet. - */ + /** Deduplicates by peer and a hash of packet type, sender, timestamp, and full payload. */ private fun generateMessageID(packet: BitchatPacket, peerID: String): String { return "$peerID-${PacketIdUtil.computeIdHex(packet)}" } From c127eb83ab94c069c32d37530d2faecd381cd2a8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 13 Sep 2026 06:20:34 +0000 Subject: [PATCH 44/45] Automated update of relay data - Sun Sep 13 06:20:34 UTC 2026 --- app/src/main/assets/nostr_relays.csv | 745 ++++++++++++++------------- 1 file changed, 379 insertions(+), 366 deletions(-) diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index 9913b970..40c786cc 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,378 +1,391 @@ Relay URL,Latitude,Longitude -relayone.geektank.ai:443,39.0997,-94.5786 -relay.ohstr.com:443,43.6532,-79.3832 -budabit.nostr1.com,40.7057,-74.0136 -relay.flashapp.me,43.6548,-79.3885 -relay.mccormick.cx:443,52.3563,4.95714 -nostr.mom:443,50.4754,12.3683 -nostr.carroarmato0.be,51.0355,4.48285 -relay01.lnfi.network,35.6764,139.65 -relay.mwaters.net,50.9871,2.12554 -cyberspace.nostr1.com,40.7057,-74.0136 -relay.endfiat.money:443,57.7089,11.9746 -nostr.mas-family.eu,60.3478,15.7505 -nostr.hekster.org:443,37.3986,-121.964 -relay.favillakey.io,-27.4705,153.026 -21milionidinostr.duckdns.org,43.3054,13.5808 -staging.yabu.me,35.6092,139.73 -relay.lanacoin-eternity.com,40.8302,-74.1299 -relay.pocketstr.com,40.8302,-74.1299 -relay.0xchat.com,43.6532,-79.3832 -vault.iris.to,43.6532,-79.3832 -nexus.libernet.app,43.6532,-79.3832 -relay.conduit.market,38.7946,-106.535 -aurum.saturnali.net,46.2044,6.14316 -relay.loveisbitcoin.com,43.6532,-79.3832 -nostr.unkn0wn.world,46.8499,9.53287 -relay.nmail.li,50.9871,2.12554 -relay.nostrmap.net:443,60.1699,24.9384 -relay.kizuna-miki.com,35.694,139.754 -nostras.app,38.7946,-106.535 -nostr.pbfs.io:443,50.4754,12.3683 -relay.tdw.lol,41.8781,-87.6298 -relayrs.notoshi.win,43.6532,-79.3832 -relay.nostrhub.fr,48.1045,11.6004 -nostr.infero.net,35.6764,139.65 -relay5.bitransfer.org,43.6532,-79.3832 -adre.su,59.9311,30.3609 -buzz.ac2n-share.kozow.com,45.764,4.83566 -0x-nostr-relay.fly.dev,38.7946,-106.535 -relay.satlantis.io,39.0438,-77.4874 -relay.nostr.bond,40.1885,29.061 -nostr.relay.hedwig.sh,60.1699,24.9384 -relay.lightning.pub:443,39.0438,-77.4874 -relay.edufeed.org:443,49.4521,11.0767 -nostr.dlcdevkit.com,40.0992,-83.1141 -nostr.dlcdevkit.com:443,40.0992,-83.1141 -nostr.pbfs.io,50.4754,12.3683 -soloco.nl,43.6532,-79.3832 -chat-relay.zap-work.com:443,43.6532,-79.3832 -nostr.purpura.cloud,43.6532,-79.3832 -relay.nostrmap.net,60.1699,24.9384 -nostr.plantroon.com:443,50.1013,8.62643 -nostr.novacisko.cz,52.2026,20.9397 -relay.bitmacro.cloud,43.6532,-79.3832 -bitcoinostr.duckdns.org,41.1976,1.11167 -nostr.l484.com,30.2672,-97.7431 -relay.sincensura.org,43.6532,-79.3832 -nostr-relay.acloud.kdns.fr,43.6532,-79.3832 -dev-relay.nostr.space,43.6532,-79.3832 -bruh.samt.st,43.6532,-79.3832 -nostrcity-club.fly.dev:443,38.7946,-106.535 -relay.beginningend.com,35.2227,-97.4786 -relay.i9.eti.br,43.6532,-79.3832 -relay.kaleidoswap.com,50.8476,4.35717 -relay2.angor.io,48.1046,11.6002 -relay.wellorder.net,45.5201,-122.99 -nostr.2b9t.xyz:443,34.0549,-118.243 -bcast.seutoba.com.br,43.6532,-79.3832 -relay.chorus.community:443,50.7529,6.84585 -nostr.janx.com,43.6532,-79.3832 -nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 -cs-relay.nostrdev.com:443,50.4754,12.3683 -prl.plus,55.7558,37.6173 -nostr.spaceshell.xyz,43.6532,-79.3832 -relay.nostreon.com,60.1699,24.9384 -relay.nostr.net,43.6532,-79.3832 -nos.lol:443,50.4754,12.3683 -articles.layer3.news:443,37.3387,-121.885 -relay.manneken.brussels,49.4543,11.0746 -bucket.coracle.social,37.7775,-122.397 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -relay.getvia.xyz,60.1699,24.9384 -nostrmxn.lulus.com.mx,59.4016,17.9455 -nostr.oxtr.dev,50.4754,12.3683 -relay.mitosystem.net,-23.5558,-46.6396 -relayone.soundhsa.com:443,39.0997,-94.5786 -relay.nuts.cash,52.3676,4.90414 -nostr.snowbla.de:443,50.4754,12.3683 -strfry.bonsai.com,39.0438,-77.4874 -relay.edufeed.org,49.4521,11.0767 -nostr.sathoarder.com,48.5734,7.75211 -relay.paulstephenborile.com,49.4543,11.0746 -x.kojira.io,43.6532,-79.3832 -relay.keykeeper.world,40.7824,-74.0711 -relay.qstr.app,50.1109,8.68213 -nostr.snowbla.de,50.4754,12.3683 -wisp.djorivaltech.com,43.6532,-79.3832 -relayone.geektank.ai,39.0997,-94.5786 -relay.ditto.pub:443,43.6532,-79.3832 -relay.staging.plebeian.market,51.5072,-0.127586 -relay.nostrcheck.me,43.6532,-79.3832 -nostr.planix.org,43.6532,-79.3832 -nostr.plantroon.com,50.1013,8.62643 -nostr.islandarea.net,35.4669,-97.6473 -relay.nostrfeed.com,60.1699,24.9384 -relay.mitchelltribe.com,39.0438,-77.4874 -relay.fiatdenier.com,43.7787,-79.5393 -nostr-relay.xbytez.io,50.6924,3.20113 -nostr.hekster.org,37.3986,-121.964 -relay.laantungir.net,-19.4692,-42.5315 -nostr.openhoofd.nl,51.5717,3.70417 -nostr.computingcache.com,45.5341,-122.956 -relay.endfiat.money,57.7089,11.9746 -dev.relay.stream,43.6532,-79.3832 -nostr.wecsats.io,43.6532,-79.3832 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -relay.nostu.be,40.4167,-3.70329 -nostr.rtvslawenia.com:443,49.4543,11.0746 -nostr.ltd,19.076,72.8777 -relay.veganostr.com:443,60.1699,24.9384 -relay.shadowbip.com,54.352,18.6466 -denimroad.feeds.relay.tools,38.6327,-90.1961 -relay.arx-ccn.com,50.4754,12.3683 -nostr-relay.corb.net,39.6478,-104.988 -relay.cosmicbolt.net:443,37.3986,-121.964 -nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 -nostrride.io,37.3986,-121.964 -nr.yay.so,46.2126,6.1154 -wheat.happytavern.co,43.6532,-79.3832 -qr.lostcause.nohost.me,-33.784,151.121 -nostr.bitcoiner.social,47.6743,-117.112 -relay.cypherflow.ai,48.8575,2.35138 -relay1.privkey.io,25.7617,-80.1918 -relay.sector01.com,41.4513,-81.7021 -strfry.apps3.slidestr.net,40.4167,-3.70329 -relay.froth.zone,60.1699,24.9384 -nostr-verified.wellorder.net,45.5201,-122.99 -relay.islandbitcoin.com,20.3898,78.0903 -n.musicroadmap.com,35.694,139.754 -relay.gulugulu.moe:443,43.6532,-79.3832 -fanfares.nostr1.com,40.7057,-74.0136 -nostr.carroarmato0.be:443,51.0355,4.48285 -conduitl2.fly.dev,38.7946,-106.535 -relay.hivetalk.org,40.8302,-74.1299 -nostr.hoppe-relay.it.com,42.8864,-78.8784 -qr.3nostr.com,42.8864,-78.8784 -strfry.shock.network,39.0438,-77.4874 -nostr.yutakobayashi.com,43.6532,-79.3832 -relay.plebeian.market,50.1109,8.68213 -auth.nostr1.com,40.7057,-74.0136 -nostr-relay.cbrx.io,43.6532,-79.3832 -relay.lanavault.space,60.1699,24.9384 -nostr.girino.org,43.6532,-79.3832 -espelho.girino.org,43.6532,-79.3832 -bendernostur.duckdns.org:8443,50.1109,8.68213 -nostr.whitenode45.ddns.net,40.55,-74.4758 -relay.scuba323.com,40.8218,-74.45 -relay.chatbett.de,40.7128,-74.006 -relay.decentralia.fr,48.122,11.589 -relay.nostx.io,43.6532,-79.3832 -relay.mypathtofire.de,42.8864,-78.8784 -relay.wasabiwallet.io,43.6532,-79.3832 -tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 -dev-relay.nostreon.com,60.1699,24.9384 -relay.directsponsor.net,42.8864,-78.8784 -nostr.4rs.nl,49.0291,8.35696 -nostr.tbxnetworx.de,47.8267,10.0205 -nip85.nosfabrica.com,39.0997,-94.5786 -relayrs.notoshi.win:443,43.6532,-79.3832 -nostr.christiansass.de,50.1109,8.68213 -nostr-pub.wellorder.net,45.5201,-122.99 -relay.fountain.fm:443,43.6532,-79.3832 -nostr.txlyre.website,60.1699,24.9384 -bitcoiner.social,47.6743,-117.112 -relay.wavlake.com,41.2619,-95.8608 -relay.internationalright-wing.org,-22.4692,-48.9875 -relay.samt.st,40.8302,-74.1299 -nostr.overmind.lol:443,43.6532,-79.3832 -relay2.orangesync.tech,40.7128,-74.006 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -relay.lightning.pub,39.0438,-77.4874 -schnorr.me,43.6532,-79.3832 -nostr.thalheim.io,60.1699,24.9384 -nostr.21crypto.ch,47.5356,8.73209 -openrelay.ziomc.com,45.4642,9.18998 -relay.underorion.se,50.1109,8.68213 -nostr-relay.zimage.com,34.282,-118.439 -relay.orangesync.tech,40.7128,-74.006 -nostr.n7ekb.net,47.4941,-122.294 -relay.kilombino.com,43.6532,-79.3832 -relay.s-w.art,43.6532,-79.3832 -relay.mleku.dev,32.7767,-96.797 -nostr.myshosholoza.co.za,52.3676,4.90414 -nostr.fullstackcash.net,45.5201,-122.99 -nostr.na.social,43.6532,-79.3832 -nostr.chaima.info,51.5072,-0.127586 -groups.beginningend.com,35.2227,-97.4786 -relay.zone667.com,60.1699,24.9384 -nostr.spicyz.io,43.6532,-79.3832 -nostr.islandarea.net:443,35.4669,-97.6473 -no.str.cr,8.96171,-83.5246 -nostr.wild-vibes.ts.net,48.8566,2.35222 -relay.veganostr.com,60.1699,24.9384 -relay.btcforplebs.com,43.6532,-79.3832 -relay.fountain.fm,43.6532,-79.3832 -nostr.linky.fit,50.1109,8.68213 -relay.nostr.dev.br,48.1046,11.6002 -relay.agentry.com,42.8864,-78.8784 -nostr.wecsats.io:443,43.6532,-79.3832 -syb.lol,34.0549,-118.243 myvoiceourstory.org,37.3598,-121.981 -relay.sigit.io,50.4754,12.3683 -relay.bornheimer.app,50.1109,8.68213 -nostr.data.haus:443,50.4754,12.3683 -talon.quest,43.6532,-79.3832 -fanfares.nostr1.com:443,40.7057,-74.0136 -chorus.mikedilger.com:444,-36.8906,174.794 -relay.nexterz.com,43.6532,-79.3832 -relay.aarpia.com,37.3986,-121.964 -nostr.mikoshi.de,50.1109,8.68213 -relay.wisp.talk,49.4543,11.0746 -relay.jmoose.rocks,32.8009,-96.8195 -relay.vrtmrz.net:443,43.6532,-79.3832 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -relay.satsmarkt.club,52.6907,4.8181 -relay.gulugulu.moe,43.6532,-79.3832 -nostr.stakey.net,52.3676,4.90414 -relay.bullishbounty.com,43.6532,-79.3832 -nostr.debate.report,50.1109,8.68213 -node.kommonzenze.de,49.4521,11.0767 -relay.chorus.community,50.7529,6.84585 -nostr-relay.nextblockvending.com,47.2343,-119.853 -relayone.soundhsa.com,39.0997,-94.5786 -bcast.girino.org,43.6532,-79.3832 -bitsat.molonlabe.holdings,51.4012,-1.3147 -relay.nostr.com,50.1109,8.68213 -purplerelay.com,43.6532,-79.3832 -relay.plebeian.market:443,50.1109,8.68213 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -offchain.pub,39.1585,-94.5728 -relay.cosmicbolt.net,37.3986,-121.964 -nostr.rtvslawenia.com,49.4543,11.0746 -relay.nostrops.com,32.71,-96.6745 -nos.lol,50.4754,12.3683 -relay-na1.metanomalist.com,43.6532,-79.3832 -relay.primal.net,43.6532,-79.3832 -nostr.davenov.com,50.1109,8.68213 -nostr.nodesmap.com,59.3327,18.0656 -nostr.bitcoiner.social:443,47.6743,-117.112 -familiamartins.net.br,-22.8269,-43.0539 -dm-test-strfry-generic.samt.st,43.6532,-79.3832 -nostr.tagomago.me,42.3601,-71.0589 -relay.mitchelltribe.com:443,39.0438,-77.4874 -nostr.tabordalab.com,60.1699,24.9384 -relay.staging.plebeian.market:443,51.5072,-0.127586 -relay.orly.dev,32.7767,-96.797 -relay.pyramid.li,47.4093,8.46503 -relay.dyne.org,49.0291,8.35705 -nostr.thalheim.io:443,60.1699,24.9384 -relay.routstr.com,59.4016,17.9455 -wot.sudocarlos.com,43.7696,-79.3309 -relay.dreamith.to,43.6532,-79.3832 -relay.opmaat.org,60.1699,24.9384 -relay.ezernode.net,52.6921,6.19372 -relay.sigit.io:443,50.4754,12.3683 -relay.minibolt.info,43.6532,-79.3832 -relay.trotters.cc:443,43.6532,-79.3832 -nostr-relay.corb.net:443,39.6478,-104.988 -relay2.fiatdenier.com,50.1013,8.62643 -relay.openspecs.uid.ovh,50.9871,2.12554 -nostrcity-club.fly.dev,38.7946,-106.535 -nostr.myshosholoza.co.za:443,52.3676,4.90414 -strfry.shock.network:443,39.0438,-77.4874 -relay.nearhood.co.uk,51.5134,-0.0890675 -offchain.pub:443,39.1585,-94.5728 -cs-relay.nostrdev.com,50.4754,12.3683 -offchain.bostr.online,43.6532,-79.3832 -relay.bullishbounty.com:443,43.6532,-79.3832 -relay.wisp.talk:443,49.4543,11.0746 -relay2.veganostr.com,60.1699,24.9384 -relay.mappingbitcoin.com,43.6532,-79.3832 -nostr.liberty.fans,36.8767,-89.5879 -kotukonostr.onrender.com,37.7775,-122.397 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -nostr.hifish.org,47.4009,8.57814 -nostr.stakey.net:443,52.3676,4.90414 -relay.mccormick.cx,52.3563,4.95714 -relay.paulstephenborile.com:443,49.4543,11.0746 -relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -nostrelay.circum.space:443,52.2245,8.826 -relay.nostriot.com,41.5695,-83.9786 -schnorr.me:443,43.6532,-79.3832 -nostr.bond,50.1109,8.68213 -temp.iris.to,43.6532,-79.3832 -cache.trustr.ing,43.6548,-79.3885 -relay.snotr.nl:49999,51.9758,4.31389 -relay.bitos.space,43.6532,-79.3832 -nostr.azzamo.net,52.2633,21.0283 -relay.dwadziesciajeden.pl,52.2297,21.0122 -relay.tdw.gg,34.0549,-118.243 -relay.44billion.net,43.6532,-79.3832 -maxq.descendant.io,43.6532,-79.3832 -public.crostr.com:443,43.6532,-79.3832 -nostr.easycryptosend.it,43.6532,-79.3832 -relay.ltgnet.work:8443,51.0511,-114.075 -nostr2.girino.org:443,43.6532,-79.3832 -ynostr.yael.at,60.1699,24.9384 -nostr.azzamo.net:443,52.2633,21.0283 -nrs-01.darkcloudarcade.com,39.0997,-94.5786 -relay.sharegap.net,43.6532,-79.3832 -relay.guggero.org,46.5971,9.59652 -relay.pprgb.app,52.52,13.405 -relay.jmoose.rocks:443,32.8009,-96.8195 -relay.klabo.world,47.674,-122.122 -nostr.chaima.info:443,51.5072,-0.127586 -nostr-02.uid.ovh,50.9871,2.12554 -nostr.tac.lol,47.4748,-122.273 -relay.nostrian-conquest.com,41.223,-111.974 -hasky.chat,40.7862,-74.0743 -relay.satmaxt.xyz,43.6532,-79.3832 -bridge.tagomago.me,42.3601,-71.0589 -treuzkas.branruz.com,48.8575,2.35138 -relay.illuminodes.com,43.6532,-79.3832 -relay.damus.io,43.6532,-79.3832 -nostr.twinkle.lol,51.902,7.6657 -relay.getsafebox.app:443,43.6532,-79.3832 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -nostr.sovereignservices.xyz,43.6532,-79.3832 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -vm-1734.lnvps.cloud,53.3498,-6.26031 +nostr.whitenode45.ddns.net,40.55,-74.4758 +relay5.bitransfer.org,43.6532,-79.3832 +relay.littlebitstudios.com,43.6532,-79.3832 public.crostr.com,43.6532,-79.3832 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 -nostr.emanuelemiani.it,45.778,8.79414 -relay.ru.ac.th,13.7607,100.627 -relay.piazza.today,48.122,11.589 -support.flotilla.social,32.9483,-96.7299 -nostr-01.uid.ovh,50.9871,2.12554 -freelay.sovbit.host,60.1699,24.9384 -nostr.data.haus,50.4754,12.3683 -nostr-01.yakihonne.com:443,1.32123,103.695 +nostr-verified.wellorder.net,45.5201,-122.99 +nostr-relay.xbytez.io,50.6924,3.20113 +nostr.azzamo.net:443,52.2633,21.0283 +yabu.me,35.6092,139.73 +relay.arx-ccn.com,50.4754,12.3683 +relay.ohstr.com:443,43.6532,-79.3832 +relay.nearhood.co.uk,51.5134,-0.0890675 +purplerelay.com,43.6532,-79.3832 +cs-relay.nostrdev.com,50.4754,12.3683 +nostr.red5d.dev,43.6532,-79.3832 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +relay.lightning.pub:443,39.0438,-77.4874 +relay.mccormick.cx:443,52.3563,4.95714 +fanfares.nostr1.com:443,40.7057,-74.0136 +relay.shadowbip.com,54.352,18.6466 +nip85.nosfabrica.com,39.0997,-94.5786 +social.amanah.eblessing.co,48.1046,11.6002 +relay.openspecs.uid.ovh,50.9871,2.12554 +relay.pyramid.li,47.4093,8.46503 +bcast.girino.org,43.6532,-79.3832 +relay.jmoose.rocks:443,32.8009,-96.8195 +relay.solife.me,43.6532,-79.3832 +relay.flashapp.me,43.6548,-79.3885 relay.wavlake.com:443,41.2619,-95.8608 -no.str.cr:443,8.96171,-83.5246 +nostr.plantroon.com:443,50.1013,8.62643 +relay.lanacoin-eternity.com,40.8302,-74.1299 +nostr.infero.net,35.6764,139.65 +nostrelay.circum.space:443,52.2245,8.826 dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -relay.bowlafterbowl.com,32.9483,-96.7299 -strfry.bonsai.com:443,39.0438,-77.4874 -nostr.2b9t.xyz,34.0549,-118.243 -rele.speyhard.fi,51.5072,-0.127586 -relay.lacrypta.ar,43.6532,-79.3832 -infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -reraw.pbla2fish.cc,43.6532,-79.3832 -relay.nostr.space,43.6532,-79.3832 -relay.atsocy.com,43.6532,-79.3832 -nostr-relay.xbytez.io:443,50.6924,3.20113 -relay.earthly.city,34.1749,-118.54 +relay2.fiatdenier.com,50.1013,8.62643 +nostr.l484.com,30.2672,-97.7431 +relay.nostr.net,43.6532,-79.3832 +relay.nuts.cash,52.3676,4.90414 +nostr.bond,50.1109,8.68213 +nostr.debate.report,50.1109,8.68213 +prl.plus,55.7558,37.6173 +temp.iris.to,43.6532,-79.3832 relay.noeudlibre.fr,50.6924,3.20113 -inbox.scuba323.com,40.8218,-74.45 -relay.getsafebox.app,43.6532,-79.3832 -relay.cyberguy.fyi,52.6907,4.8181 +relay.routstr.com,59.4016,17.9455 +relay.islandbitcoin.com,20.3898,78.0903 +nostr.linky.fit,50.1109,8.68213 bitchat.nostr1.com,40.7057,-74.0136 -nostrelay.circum.space,52.2245,8.826 +relay.lanavault.space,60.1699,24.9384 +relayone.geektank.ai:443,39.0997,-94.5786 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +relay.kizuna-miki.com,35.694,139.754 +relay.edufeed.org:443,49.4521,11.0767 +nr.yay.so,46.2126,6.1154 nostr.oxtr.dev:443,50.4754,12.3683 -relay.degmods.com,50.4754,12.3683 -relay.ohstr.com,43.6532,-79.3832 -relay.bnos.space,43.6532,-79.3832 -relay.novospes.com,43.6532,-79.3832 -relay.satsapp.me,50.4754,12.3683 -nostr.sathoarder.com:443,48.5734,7.75211 -nostr-01.yakihonne.com,1.32123,103.695 +relay.primal.net,43.6532,-79.3832 +relay.staging.plebeian.market,51.5072,-0.127586 +nostr.planix.org,43.6532,-79.3832 +relay.artio.inf.unibe.ch,46.9501,7.43678 +chorus.mikedilger.com:444,-36.8906,174.794 +nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 +relay.pocketstr.com,40.8302,-74.1299 +nostr.middling.mydns.jp,35.8099,140.12 +offchain.pub:443,39.1585,-94.5728 +cache.trustr.ing,43.6548,-79.3885 +nostrride.io,37.3986,-121.964 +relay.nostrfy.org,35.6916,139.768 +wheat.happytavern.co,43.6532,-79.3832 +relayone.soundhsa.com:443,39.0997,-94.5786 +relay.satsmarkt.club,52.6907,4.8181 +nexus.libernet.app,43.6532,-79.3832 +relay.directsponsor.net,42.8864,-78.8784 +nostr.spicyz.io,43.6532,-79.3832 +nostr.rtvslawenia.com,49.4543,11.0746 relay.trotters.cc,43.6532,-79.3832 -relay.ditto.pub,43.6532,-79.3832 -relay.angor.io,48.1046,11.6002 -nostr.mom,50.4754,12.3683 -nostr.vulpem.com,49.4543,11.0746 +relayone.soundhsa.com,39.0997,-94.5786 +nostr.na.social,43.6532,-79.3832 +ribo.us.nostria.app:443,43.6532,-79.3832 +nostr.hekster.org,37.3986,-121.964 +relay.nostr.dev.br,48.1046,11.6002 +relay.keykeeper.world,40.7824,-74.0711 +nostr.pbfs.io,50.4754,12.3683 +nostr.girino.org,43.6532,-79.3832 +relay1.privkey.io,25.7617,-80.1918 +relay.sharegap.net,43.6532,-79.3832 +relay.dreamith.to,43.6532,-79.3832 +nostr2.girino.org,43.6532,-79.3832 +nostr.bitcoiner.social,47.6743,-117.112 +relay.nostu.be,40.4167,-3.70329 +relay.mwaters.net,50.9871,2.12554 +nostr.islandarea.net,35.4669,-97.6473 +chorus.bonsai.com,39.0438,-77.4874 +nostr.yutakobayashi.com,43.6532,-79.3832 +nostras.app,38.7946,-106.535 +relay.yoinekodo.jp,43.6532,-79.3832 +top.testrelay.top,43.6532,-79.3832 +nostr-01.uid.ovh,50.9871,2.12554 +relay.orly.dev,32.7767,-96.797 +offchain.bostr.online,43.6532,-79.3832 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 +conduitl2.fly.dev,38.7946,-106.535 +nostr.n7ekb.net,47.4941,-122.294 +relay.44billion.net,43.6532,-79.3832 +nostrcity-club.fly.dev,38.7946,-106.535 +syb.lol,34.0549,-118.243 +relay.mypathtofire.de,42.8864,-78.8784 +nostr.relay.hedwig.sh,60.1699,24.9384 +nostr.myshosholoza.co.za:443,52.3676,4.90414 +relay.minibolt.info,43.6532,-79.3832 +nostr.fullstackcash.net,45.5201,-122.99 +bendernostur.duckdns.org:8443,50.1109,8.68213 +nostr.21crypto.ch,47.5356,8.73209 +relay.qstr.app,50.1109,8.68213 +ynostr.yael.at,60.1699,24.9384 +nostr.stakey.net,52.3676,4.90414 +espelho.girino.org,43.6532,-79.3832 +nostr.snowbla.de:443,50.4754,12.3683 +hasky.chat,40.7862,-74.0743 +relay.plebeian.market,50.1109,8.68213 +relay.zone667.com,60.1699,24.9384 +cs-relay.nostrdev.com:443,50.4754,12.3683 +relay.staging.plebeian.market:443,51.5072,-0.127586 +relay.openresist.com,43.6532,-79.3832 +nostr.spaceshell.xyz,43.6532,-79.3832 +vault.iris.to:443,43.6532,-79.3832 +nostr.easycryptosend.it,43.6532,-79.3832 +nostr.bitcoiner.social:443,47.6743,-117.112 +relay.fiatdenier.com,43.7787,-79.5393 +relay.gulugulu.moe:443,43.6532,-79.3832 +relay.mitosystem.net,-23.5558,-46.6396 +relay.mostro.network,40.8302,-74.1299 +relay1.nostrchat.io,60.1699,24.9384 +nostr.tabordalab.com,60.1699,24.9384 +relay.gulugulu.moe,43.6532,-79.3832 +pocketrelay.live,40.8302,-74.1299 +relay.nostrfeed.com,60.1699,24.9384 +testr.nymble.world,40.8054,-74.0241 +x.kojira.io,43.6532,-79.3832 +relay.sigit.io,50.4754,12.3683 +nostrcity-club.fly.dev:443,38.7946,-106.535 +relay.wisp.talk:443,49.4543,11.0746 +relay-testnet.k8s.layer3.news,37.3387,-121.885 nostr-dev.wellorder.net,45.5201,-122.99 +nostr.pbfs.io:443,50.4754,12.3683 +nostr.takasaki.dev,43.6532,-79.3832 +relay.snort.social,53.3498,-6.26031 +articles.layer3.news,37.3387,-121.885 +staging.yabu.me,35.6092,139.73 +nostr.rtvslawenia.com:443,49.4543,11.0746 +nostr-relay.xbytez.io:443,50.6924,3.20113 +public.crostr.com:443,43.6532,-79.3832 +nostr.ltd,19.076,72.8777 +nostr-02.uid.ovh,50.9871,2.12554 +nostr.islandarea.net:443,35.4669,-97.6473 +relay.kilombino.com,43.6532,-79.3832 +relay.satlantis.io,39.0438,-77.4874 +nostr.computingcache.com,45.5341,-122.956 +nostr.wecsats.io,43.6532,-79.3832 +nostr-relay.zimage.com,34.282,-118.439 +nostr.tbxnetworx.de,47.8226,10.0064 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +relay.klabo.world,47.674,-122.122 +auth.nostr1.com,40.7057,-74.0136 +relay.degmods.com,50.4754,12.3683 +relay.olas.app,60.1699,24.9384 +fanfares.nostr1.com,40.7057,-74.0136 +node.kommonzenze.de,49.4521,11.0767 +relay.bitos.space,43.6532,-79.3832 +relay01.lnfi.network,35.6764,139.65 +relay.froth.zone,60.1699,24.9384 +denimroad.feeds.relay.tools,38.6327,-90.1961 +nostr.myshosholoza.co.za,52.3676,4.90414 +21milionidinostr.duckdns.org,41.8945,12.6493 +relay.getvia.xyz,60.1699,24.9384 +ribo.us.nostria.app,43.6532,-79.3832 +relay.nostrhub.fr,48.1045,11.6004 +talon.quest,43.6532,-79.3832 +portal-relay.pareto.space,49.4521,11.0767 +relayone.geektank.ai,39.0997,-94.5786 +relay.orangesync.tech,40.7128,-74.006 +strfry.apps3.slidestr.net,40.4167,-3.70329 +relay.mleku.dev,32.7767,-96.797 +relay.nostrian-conquest.com,41.223,-111.974 +relay.underorion.se,50.1109,8.68213 +vault.iris.to,43.6532,-79.3832 +schnorr.me,43.6532,-79.3832 +relay.bullishbounty.com:443,43.6532,-79.3832 +relay.i9.eti.br,43.6532,-79.3832 +nostr-pub.wellorder.net,45.5201,-122.99 +relay.cyberguy.fyi,52.6907,4.8181 +relay.mappingbitcoin.com,43.6532,-79.3832 +nostr-relay.acloud.kdns.fr,43.6532,-79.3832 +nostr.2b9t.xyz,34.0549,-118.243 +nostr.2b9t.xyz:443,34.0549,-118.243 +dev-relay.nostreon.com,60.1699,24.9384 +nostr.purpura.cloud,43.6532,-79.3832 +nos.lol:443,50.4754,12.3683 +nostr-relay.corb.net,39.6478,-104.988 +relay.scuba323.com,40.8218,-74.45 +relay.agora.social,50.7383,15.0648 +nostr.data.haus,50.4754,12.3683 +relay.satsapp.me,50.4754,12.3683 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +nostr.hekster.org:443,37.3986,-121.964 +relay.btcforplebs.com,43.6532,-79.3832 +relay-arg.zombi.cloudrodion.com,1.35208,103.82 +nostr.wild-vibes.ts.net,48.8566,2.35222 +relay.bornheimer.app,50.1109,8.68213 +relay02.lnfi.network,35.6764,139.65 +relay.kaleidoswap.com,50.8476,4.35717 +strfry.shock.network,39.0438,-77.4874 +nostr.txlyre.website,60.1699,24.9384 +nostr.twinkle.lol,51.902,7.6657 +relay.satmaxt.xyz,43.6532,-79.3832 +relay.vrtmrz.net:443,43.6532,-79.3832 +aurum.saturnali.net,46.2044,6.14316 +nostrrelay.taylorperron.com,45.5029,-73.5723 +nostrelay.circum.space,52.2245,8.826 +relay.mostr.pub:443,43.6532,-79.3832 +nostr.data.haus:443,50.4754,12.3683 +relay.nostreon.com,60.1699,24.9384 +relay.plebeian.market:443,50.1109,8.68213 +relay.damus.io,43.6532,-79.3832 +budabit.nostr1.com,40.7057,-74.0136 +bucket.coracle.social,37.7775,-122.397 +relay.chorus.community:443,50.7529,6.84585 +relay.hivetalk.org,40.8302,-74.1299 +relay.favillakey.io,-27.4705,153.026 +nostr.mom,50.4754,12.3683 +relay2.orangesync.tech,40.7128,-74.006 +relay.artiostr.ch,43.6532,-79.3832 +relay.lacrypta.ar,43.6532,-79.3832 +treuzkas.branruz.com,48.8575,2.35138 +relay2.veganostr.com,60.1699,24.9384 +borgar.lol,52.3676,4.90414 +relay.ltgnet.work:8443,51.0511,-114.075 +relay.nostrmap.net,60.1699,24.9384 +nostr.mom:443,50.4754,12.3683 +relay.decentralia.fr,48.122,11.589 +relay.atsocy.com,43.6532,-79.3832 +strfry.shock.network:443,39.0438,-77.4874 +relay.vrtmrz.net,43.6532,-79.3832 +0x-nostr-relay.fly.dev,38.7946,-106.535 +relay.veganostr.com,60.1699,24.9384 +relay.nostr.bond,40.1885,29.061 +relay.angor.io,48.1046,11.6002 +nostr.sathoarder.com:443,48.5734,7.75211 +nostr.oxtr.dev,50.4754,12.3683 +dev.relay.stream,43.6532,-79.3832 +relay.mitchelltribe.com,39.0438,-77.4874 +nostr-relay.cbrx.io,43.6532,-79.3832 +nostr2.girino.org:443,43.6532,-79.3832 +relay.snotr.nl:49999,51.9758,4.31389 +n.musicroadmap.com,35.694,139.754 +relay.loveisbitcoin.com,43.6532,-79.3832 +relay.conduit.market,38.7946,-106.535 +schnorr.me:443,43.6532,-79.3832 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +nostr.dlcdevkit.com,40.0992,-83.1141 +relay2.angor.io,48.1046,11.6002 +nostr.hifish.org,47.4009,8.57814 +rele.speyhard.fi,51.5072,-0.127586 +nostr.chaima.info:443,51.5072,-0.127586 +relay.trotters.cc:443,43.6532,-79.3832 +nostrmxn.lulus.com.mx,59.4016,17.9455 +chat-relay.zap-work.com:443,43.6532,-79.3832 +relay.tdw.gg,34.0549,-118.243 +relay.ohstr.com,43.6532,-79.3832 +relay.guggero.org,46.5971,9.59652 +nostr.stakey.net:443,52.3676,4.90414 +dev-relay.nostr.space,43.6532,-79.3832 +nostr.snowbla.de,50.4754,12.3683 +relay.piazza.today,48.122,11.589 +relay.fountain.fm:443,43.6532,-79.3832 +relay-dev.gulugulu.moe,43.6532,-79.3832 +relay.ditto.pub:443,43.6532,-79.3832 +vm-1734.lnvps.cloud,53.3498,-6.26031 +relay.beginningend.com,35.2227,-97.4786 +nos.lol,50.4754,12.3683 +nostr-01.yakihonne.com:443,1.32123,103.695 +relay.sigit.io:443,50.4754,12.3683 +nostr.janx.com,43.6532,-79.3832 +relay.wavlake.com,41.2619,-95.8608 +relayrs.notoshi.win:443,43.6532,-79.3832 +openrelay.ziomc.com,45.4642,9.18998 +nostr.88mph.life,52.1941,-2.21905 +maxq.descendant.io,43.6532,-79.3832 +nostr.wecsats.io:443,43.6532,-79.3832 +relay.bitmacro.cloud,43.6532,-79.3832 +relay.lightning.pub,39.0438,-77.4874 +strfry.bonsai.com,39.0438,-77.4874 +chat-relay.zap-work.com,43.6532,-79.3832 +relay.illuminodes.com,43.6532,-79.3832 +nostr.thebiglake.org,32.71,-96.6745 +ribo.eu.nostria.app,43.6532,-79.3832 +relay.novospes.com,43.6532,-79.3832 nostr.notribe.net,40.8302,-74.1299 -public.obelisk.ar,43.6532,-79.3832 +nostr-relay.corb.net:443,39.6478,-104.988 +relay.pprgb.app,52.52,13.405 +relay.wellorder.net,45.5201,-122.99 +relay.mitchelltribe.com:443,39.0438,-77.4874 +cyberspace.nostr1.com,40.7057,-74.0136 +bruh.samt.st,43.6532,-79.3832 +nostr.davenov.com,50.1109,8.68213 +nostr.sovereignservices.xyz,43.6532,-79.3832 +relay-dev.gulugulu.moe:443,43.6532,-79.3832 +nostr.unkn0wn.world,46.8499,9.53287 +relay.nostr.space,43.6532,-79.3832 +infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 +relay.ezernode.net,52.6921,6.19372 +no.str.cr,8.96171,-83.5246 +spamspamspamspam.rest,43.6532,-79.3832 +relay.ru.ac.th,13.7607,100.627 +relay.tdw.lol,41.8781,-87.6298 +buzz.cashu.space,50.1109,8.68213 +relay.duck1123.com,43.6532,-79.3832 +relay.tapestry.ninja,40.8054,-74.0241 +relay.layer.systems,49.0291,8.35695 +relay.dyne.org,49.0291,8.35705 +relay.aidatanorge.no,43.6532,-79.3832 +relay.opmaat.org,60.1699,24.9384 +nostr.azzamo.net,52.2633,21.0283 +relay.mccormick.cx,52.3563,4.95714 +relay.nostr.com,50.1109,8.68213 +nostr.plantroon.com,50.1013,8.62643 +support.flotilla.social,32.9483,-96.7299 +relay.veganostr.com:443,60.1699,24.9384 +relay.notoshi.win,12.9333,100.883 +relay.edufeed.org,49.4521,11.0767 +memlay.v0l.io,53.3498,-6.26031 +bitcoiner.social,47.6743,-117.112 +relay.manneken.brussels,49.4543,11.0746 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +no.str.cr:443,8.96171,-83.5246 +relay.cosmicbolt.net,37.3986,-121.964 +relay.getsafebox.app:443,43.6532,-79.3832 +relay.aarpia.com,37.3986,-121.964 +nostr.sathoarder.com,48.5734,7.75211 +relay.getsafebox.app,43.6532,-79.3832 +nostr.chaima.info,51.5072,-0.127586 +relay.ditto.pub,43.6532,-79.3832 +nostr.4rs.nl,49.0291,8.35696 +rilo.nostria.app,43.6532,-79.3832 +nostr.christiansass.de,51.7883,6.13865 +relay.paulstephenborile.com:443,49.4543,11.0746 +relay.earthly.city,34.1749,-118.54 +inbox.scuba323.com,40.8218,-74.45 +nostr-01.yakihonne.com,1.32123,103.695 +articles.layer3.news:443,37.3387,-121.885 +offchain.pub,39.1585,-94.5728 +strfry.bonsai.com:443,39.0438,-77.4874 +relay.nostriot.com,41.5695,-83.9786 +relay.bullishbounty.com,43.6532,-79.3832 +dm-test-strfry-generic.samt.st,43.6532,-79.3832 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +nostr.emanuelemiani.it,45.778,8.79414 +reraw.pbla2fish.cc,43.6532,-79.3832 +relay.s-w.art,43.6532,-79.3832 +relay.nostrcheck.me,43.6532,-79.3832 +relay.npubhaus.com,43.6532,-79.3832 +nostr.vulpem.com,49.4543,11.0746 +relay.wasabiwallet.io,43.6532,-79.3832 +nostr.liberty.fans,36.8767,-89.5879 +relay.paulstephenborile.com,49.4543,11.0746 +relay.nostrmap.net:443,60.1699,24.9384 +tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 +relay-na1.metanomalist.com,43.6532,-79.3832 +relay.agentry.com,42.8864,-78.8784 +relay.samt.st,40.8302,-74.1299 +relay.nostx.io,43.6532,-79.3832 +relay.cosmicbolt.net:443,37.3986,-121.964 +relayrs.notoshi.win,43.6532,-79.3832 +adre.su,59.9311,30.3609 +nostrcheck.me,43.6532,-79.3832 +relay-us.zombi.cloudrodion.com,40.7862,-74.0743 +nrs-01.darkcloudarcade.com,39.0997,-94.5786 +relay.nmail.li,50.9871,2.12554 +relay.wisp.talk,49.4543,11.0746 relay.mostr.pub,43.6532,-79.3832 +relay.jmoose.rocks,32.8009,-96.8195 +relay.laantungir.net,-19.4692,-42.5315 +nostr.nodesmap.com,59.3327,18.0656 +freelay.sovbit.host,60.1699,24.9384 +soloco.nl,43.6532,-79.3832 +ribo.nostria.app,43.6532,-79.3832 +relay.chatbett.de,40.7128,-74.006 +groups.beginningend.com,35.2227,-97.4786 +relay.chorus.community,50.7529,6.84585 +wisp.djorivaltech.com,43.6532,-79.3832 +nostr-02.yakihonne.com,1.32123,103.695 From f986b195e8f8e908d0eeeb35817ccea3a1adb93b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 20 Sep 2026 06:16:50 +0000 Subject: [PATCH 45/45] Automated update of relay data - Sun Sep 20 06:16:50 UTC 2026 --- app/src/main/assets/nostr_relays.csv | 743 +++++++++++++-------------- 1 file changed, 363 insertions(+), 380 deletions(-) diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index 40c786cc..cfa89764 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,391 +1,374 @@ Relay URL,Latitude,Longitude -myvoiceourstory.org,37.3598,-121.981 -nostr.whitenode45.ddns.net,40.55,-74.4758 -relay5.bitransfer.org,43.6532,-79.3832 -relay.littlebitstudios.com,43.6532,-79.3832 -public.crostr.com,43.6532,-79.3832 -nostr-verified.wellorder.net,45.5201,-122.99 -nostr-relay.xbytez.io,50.6924,3.20113 -nostr.azzamo.net:443,52.2633,21.0283 -yabu.me,35.6092,139.73 -relay.arx-ccn.com,50.4754,12.3683 -relay.ohstr.com:443,43.6532,-79.3832 -relay.nearhood.co.uk,51.5134,-0.0890675 -purplerelay.com,43.6532,-79.3832 -cs-relay.nostrdev.com,50.4754,12.3683 -nostr.red5d.dev,43.6532,-79.3832 -relay-fra.zombi.cloudrodion.com,48.8566,2.35222 -relay.lightning.pub:443,39.0438,-77.4874 -relay.mccormick.cx:443,52.3563,4.95714 -fanfares.nostr1.com:443,40.7057,-74.0136 -relay.shadowbip.com,54.352,18.6466 -nip85.nosfabrica.com,39.0997,-94.5786 -social.amanah.eblessing.co,48.1046,11.6002 -relay.openspecs.uid.ovh,50.9871,2.12554 -relay.pyramid.li,47.4093,8.46503 -bcast.girino.org,43.6532,-79.3832 -relay.jmoose.rocks:443,32.8009,-96.8195 -relay.solife.me,43.6532,-79.3832 -relay.flashapp.me,43.6548,-79.3885 -relay.wavlake.com:443,41.2619,-95.8608 -nostr.plantroon.com:443,50.1013,8.62643 -relay.lanacoin-eternity.com,40.8302,-74.1299 -nostr.infero.net,35.6764,139.65 -nostrelay.circum.space:443,52.2245,8.826 -dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 -relay2.fiatdenier.com,50.1013,8.62643 -nostr.l484.com,30.2672,-97.7431 -relay.nostr.net,43.6532,-79.3832 -relay.nuts.cash,52.3676,4.90414 -nostr.bond,50.1109,8.68213 -nostr.debate.report,50.1109,8.68213 -prl.plus,55.7558,37.6173 -temp.iris.to,43.6532,-79.3832 -relay.noeudlibre.fr,50.6924,3.20113 -relay.routstr.com,59.4016,17.9455 -relay.islandbitcoin.com,20.3898,78.0903 -nostr.linky.fit,50.1109,8.68213 -bitchat.nostr1.com,40.7057,-74.0136 -relay.lanavault.space,60.1699,24.9384 -relayone.geektank.ai:443,39.0997,-94.5786 -nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -relay.kizuna-miki.com,35.694,139.754 -relay.edufeed.org:443,49.4521,11.0767 -nr.yay.so,46.2126,6.1154 -nostr.oxtr.dev:443,50.4754,12.3683 -relay.primal.net,43.6532,-79.3832 -relay.staging.plebeian.market,51.5072,-0.127586 -nostr.planix.org,43.6532,-79.3832 -relay.artio.inf.unibe.ch,46.9501,7.43678 -chorus.mikedilger.com:444,-36.8906,174.794 -nrs-01.darkcloudarcade.com:443,39.0997,-94.5786 -relay.pocketstr.com,40.8302,-74.1299 -nostr.middling.mydns.jp,35.8099,140.12 -offchain.pub:443,39.1585,-94.5728 -cache.trustr.ing,43.6548,-79.3885 -nostrride.io,37.3986,-121.964 -relay.nostrfy.org,35.6916,139.768 -wheat.happytavern.co,43.6532,-79.3832 -relayone.soundhsa.com:443,39.0997,-94.5786 -relay.satsmarkt.club,52.6907,4.8181 -nexus.libernet.app,43.6532,-79.3832 -relay.directsponsor.net,42.8864,-78.8784 -nostr.spicyz.io,43.6532,-79.3832 -nostr.rtvslawenia.com,49.4543,11.0746 -relay.trotters.cc,43.6532,-79.3832 -relayone.soundhsa.com,39.0997,-94.5786 -nostr.na.social,43.6532,-79.3832 -ribo.us.nostria.app:443,43.6532,-79.3832 -nostr.hekster.org,37.3986,-121.964 -relay.nostr.dev.br,48.1046,11.6002 -relay.keykeeper.world,40.7824,-74.0711 -nostr.pbfs.io,50.4754,12.3683 -nostr.girino.org,43.6532,-79.3832 -relay1.privkey.io,25.7617,-80.1918 -relay.sharegap.net,43.6532,-79.3832 -relay.dreamith.to,43.6532,-79.3832 -nostr2.girino.org,43.6532,-79.3832 -nostr.bitcoiner.social,47.6743,-117.112 -relay.nostu.be,40.4167,-3.70329 -relay.mwaters.net,50.9871,2.12554 -nostr.islandarea.net,35.4669,-97.6473 -chorus.bonsai.com,39.0438,-77.4874 -nostr.yutakobayashi.com,43.6532,-79.3832 -nostras.app,38.7946,-106.535 -relay.yoinekodo.jp,43.6532,-79.3832 -top.testrelay.top,43.6532,-79.3832 -nostr-01.uid.ovh,50.9871,2.12554 -relay.orly.dev,32.7767,-96.797 -offchain.bostr.online,43.6532,-79.3832 -nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 -conduitl2.fly.dev,38.7946,-106.535 -nostr.n7ekb.net,47.4941,-122.294 -relay.44billion.net,43.6532,-79.3832 -nostrcity-club.fly.dev,38.7946,-106.535 -syb.lol,34.0549,-118.243 -relay.mypathtofire.de,42.8864,-78.8784 -nostr.relay.hedwig.sh,60.1699,24.9384 -nostr.myshosholoza.co.za:443,52.3676,4.90414 -relay.minibolt.info,43.6532,-79.3832 -nostr.fullstackcash.net,45.5201,-122.99 -bendernostur.duckdns.org:8443,50.1109,8.68213 -nostr.21crypto.ch,47.5356,8.73209 -relay.qstr.app,50.1109,8.68213 -ynostr.yael.at,60.1699,24.9384 -nostr.stakey.net,52.3676,4.90414 -espelho.girino.org,43.6532,-79.3832 -nostr.snowbla.de:443,50.4754,12.3683 -hasky.chat,40.7862,-74.0743 -relay.plebeian.market,50.1109,8.68213 -relay.zone667.com,60.1699,24.9384 -cs-relay.nostrdev.com:443,50.4754,12.3683 -relay.staging.plebeian.market:443,51.5072,-0.127586 -relay.openresist.com,43.6532,-79.3832 -nostr.spaceshell.xyz,43.6532,-79.3832 -vault.iris.to:443,43.6532,-79.3832 -nostr.easycryptosend.it,43.6532,-79.3832 -nostr.bitcoiner.social:443,47.6743,-117.112 -relay.fiatdenier.com,43.7787,-79.5393 -relay.gulugulu.moe:443,43.6532,-79.3832 -relay.mitosystem.net,-23.5558,-46.6396 -relay.mostro.network,40.8302,-74.1299 -relay1.nostrchat.io,60.1699,24.9384 -nostr.tabordalab.com,60.1699,24.9384 -relay.gulugulu.moe,43.6532,-79.3832 -pocketrelay.live,40.8302,-74.1299 -relay.nostrfeed.com,60.1699,24.9384 -testr.nymble.world,40.8054,-74.0241 -x.kojira.io,43.6532,-79.3832 -relay.sigit.io,50.4754,12.3683 -nostrcity-club.fly.dev:443,38.7946,-106.535 -relay.wisp.talk:443,49.4543,11.0746 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -nostr-dev.wellorder.net,45.5201,-122.99 -nostr.pbfs.io:443,50.4754,12.3683 -nostr.takasaki.dev,43.6532,-79.3832 -relay.snort.social,53.3498,-6.26031 -articles.layer3.news,37.3387,-121.885 -staging.yabu.me,35.6092,139.73 -nostr.rtvslawenia.com:443,49.4543,11.0746 -nostr-relay.xbytez.io:443,50.6924,3.20113 -public.crostr.com:443,43.6532,-79.3832 -nostr.ltd,19.076,72.8777 -nostr-02.uid.ovh,50.9871,2.12554 -nostr.islandarea.net:443,35.4669,-97.6473 -relay.kilombino.com,43.6532,-79.3832 -relay.satlantis.io,39.0438,-77.4874 -nostr.computingcache.com,45.5341,-122.956 -nostr.wecsats.io,43.6532,-79.3832 -nostr-relay.zimage.com,34.282,-118.439 -nostr.tbxnetworx.de,47.8226,10.0064 -relay-can.zombi.cloudrodion.com,43.6532,-79.3832 -relay.klabo.world,47.674,-122.122 auth.nostr1.com,40.7057,-74.0136 -relay.degmods.com,50.4754,12.3683 -relay.olas.app,60.1699,24.9384 -fanfares.nostr1.com,40.7057,-74.0136 -node.kommonzenze.de,49.4521,11.0767 -relay.bitos.space,43.6532,-79.3832 -relay01.lnfi.network,35.6764,139.65 -relay.froth.zone,60.1699,24.9384 -denimroad.feeds.relay.tools,38.6327,-90.1961 -nostr.myshosholoza.co.za,52.3676,4.90414 -21milionidinostr.duckdns.org,41.8945,12.6493 -relay.getvia.xyz,60.1699,24.9384 -ribo.us.nostria.app,43.6532,-79.3832 -relay.nostrhub.fr,48.1045,11.6004 -talon.quest,43.6532,-79.3832 -portal-relay.pareto.space,49.4521,11.0767 -relayone.geektank.ai,39.0997,-94.5786 -relay.orangesync.tech,40.7128,-74.006 -strfry.apps3.slidestr.net,40.4167,-3.70329 -relay.mleku.dev,32.7767,-96.797 -relay.nostrian-conquest.com,41.223,-111.974 -relay.underorion.se,50.1109,8.68213 -vault.iris.to,43.6532,-79.3832 -schnorr.me,43.6532,-79.3832 -relay.bullishbounty.com:443,43.6532,-79.3832 -relay.i9.eti.br,43.6532,-79.3832 -nostr-pub.wellorder.net,45.5201,-122.99 -relay.cyberguy.fyi,52.6907,4.8181 -relay.mappingbitcoin.com,43.6532,-79.3832 -nostr-relay.acloud.kdns.fr,43.6532,-79.3832 -nostr.2b9t.xyz,34.0549,-118.243 -nostr.2b9t.xyz:443,34.0549,-118.243 -dev-relay.nostreon.com,60.1699,24.9384 -nostr.purpura.cloud,43.6532,-79.3832 -nos.lol:443,50.4754,12.3683 -nostr-relay.corb.net,39.6478,-104.988 -relay.scuba323.com,40.8218,-74.45 -relay.agora.social,50.7383,15.0648 -nostr.data.haus,50.4754,12.3683 -relay.satsapp.me,50.4754,12.3683 -nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 -nostr.hekster.org:443,37.3986,-121.964 -relay.btcforplebs.com,43.6532,-79.3832 -relay-arg.zombi.cloudrodion.com,1.35208,103.82 -nostr.wild-vibes.ts.net,48.8566,2.35222 -relay.bornheimer.app,50.1109,8.68213 -relay02.lnfi.network,35.6764,139.65 -relay.kaleidoswap.com,50.8476,4.35717 -strfry.shock.network,39.0438,-77.4874 -nostr.txlyre.website,60.1699,24.9384 -nostr.twinkle.lol,51.902,7.6657 -relay.satmaxt.xyz,43.6532,-79.3832 +nostr-02.uid.ovh,50.9871,2.12554 +relayone.soundhsa.com,39.0997,-94.5786 +nostr.myshosholoza.co.za:443,52.3676,4.90414 +nostr.christiansass.de,54.3213,10.1349 relay.vrtmrz.net:443,43.6532,-79.3832 -aurum.saturnali.net,46.2044,6.14316 -nostrrelay.taylorperron.com,45.5029,-73.5723 -nostrelay.circum.space,52.2245,8.826 +relay.satsapp.me,50.4754,12.3683 +relay.wellorder.net,45.5201,-122.99 +relay.ditto.pub,43.6532,-79.3832 +ynostr.yael.at,60.1699,24.9384 +nostr.tac.lol,47.4748,-122.273 +relay.edufeed.org,49.4521,11.0767 +nostr.islandarea.net,35.4669,-97.6473 +relay.openspecs.uid.ovh,50.9871,2.12554 +inbox.scuba323.com,40.8218,-74.45 +x.kojira.io,43.6532,-79.3832 +relay2.veganostr.com,60.1699,24.9384 +nostr-verified.wellorder.net,45.5201,-122.99 +nostr.davenov.com,50.1109,8.68213 +relay.dreamith.to,43.6532,-79.3832 +relay.veganostr.com,60.1699,24.9384 +bitcoiner.social,47.6743,-117.112 +cache.trustr.ing,43.6548,-79.3885 +nostr.data.haus,50.4754,12.3683 +relay.binaryrobot.com,43.6532,-79.3832 +articles.layer3.news,37.3387,-121.885 +nostr.snowbla.de:443,50.4754,12.3683 +denimroad.feeds.relay.tools,38.6327,-90.1961 +relay.ohstr.com:443,43.6532,-79.3832 +relay.nostrfy.org,35.6916,139.768 +relay.klabo.world,47.674,-122.122 +soloco.nl,43.6532,-79.3832 +nexus.libernet.app,43.6532,-79.3832 +nostr.hekster.org:443,37.3986,-121.964 +relay.primal.net,43.6532,-79.3832 +relay.lanacoin-eternity.com,40.8302,-74.1299 +zencrown.relay2aeon.quest,52.3061,4.6907 +relay.gulugulu.moe:443,43.6532,-79.3832 +nostrelay.circum.space:443,52.2245,8.826 +relay5.bitransfer.org,43.6532,-79.3832 +chat-relay.zap-work.com,43.6532,-79.3832 +nostr-relay.zimage.com,34.282,-118.439 +relay.i9.eti.br,43.6532,-79.3832 +relay.lacrypta.ar,43.6532,-79.3832 +relay.satlantis.io,39.0438,-77.4874 +nostr.bitcoiner.social:443,47.6743,-117.112 +myvoiceourstory.org,37.3598,-121.981 +relay.cloistr.xyz,42.337,-77.3178 +nostr.liberty.fans,36.8767,-89.5879 +relay.zone667.com,60.1699,24.9384 +relay.getvia.xyz,60.1699,24.9384 +schnorr.me,43.6532,-79.3832 +nostr.debate.report,50.1109,8.68213 +nostr-relay.corb.net:443,39.6478,-104.988 +nip85.nosfabrica.com,39.0997,-94.5786 +relay.solife.me,43.6532,-79.3832 +relay.cypherflow.ai,48.8575,2.35138 relay.mostr.pub:443,43.6532,-79.3832 -nostr.data.haus:443,50.4754,12.3683 -relay.nostreon.com,60.1699,24.9384 -relay.plebeian.market:443,50.1109,8.68213 -relay.damus.io,43.6532,-79.3832 -budabit.nostr1.com,40.7057,-74.0136 +relay.tapestry.ninja,40.8054,-74.0241 +nostr-01.yakihonne.com,1.32123,103.695 +relay.s-w.art,43.6532,-79.3832 +nostr.azzamo.net:443,52.2633,21.0283 +relay.froth.zone,60.1699,24.9384 +relay.kizuna-miki.com,35.694,139.754 +relay.bowlafterbowl.com,32.9483,-96.7299 +top.testrelay.top,43.6532,-79.3832 +nostr-kyomu-haskell.onrender.com,37.7775,-122.397 +nostr.wecsats.io,43.6532,-79.3832 +relay1.nostrchat.io,60.1699,24.9384 +relay.gulugulu.moe,43.6532,-79.3832 +aurum.saturnali.net,46.2044,6.14316 +nostr.txlyre.website,60.1699,24.9384 +nostras.app,38.7946,-106.535 +bridge.tagomago.me,42.3601,-71.0589 +cs-relay.nostrdev.com:443,50.4754,12.3683 +21milionidinostr.duckdns.org,41.8945,12.6493 +wisp.djorivaltech.com,43.6532,-79.3832 +relay.manneken.brussels,49.4543,11.0746 +nostr.oxtr.dev,50.4754,12.3683 +nostr.n7ekb.net,47.4941,-122.294 +nostr.unkn0wn.world,46.8499,9.53287 +pocketrelay.live,40.8302,-74.1299 +scores.brainstorm.world,39.0997,-94.5786 +relay.guggero.org,46.5971,9.59652 +relay.layer.systems,49.0291,8.35695 +relay.wisp.talk,49.4543,11.0746 +bendernostur.duckdns.org:8443,50.1109,8.68213 +dev-relay.nostr.space,43.6532,-79.3832 +relay.mwaters.net,50.9871,2.12554 +strfry.shock.network,39.0438,-77.4874 +n.musicroadmap.com,35.694,139.754 +cs-relay.nostrdev.com,50.4754,12.3683 +talon.quest,43.6532,-79.3832 +vidar.nestoris.online,59.3911,17.8752 +nostr.red5d.dev,43.6532,-79.3832 +nostr.rtvslawenia.com:443,49.4543,11.0746 +nostr.2b9t.xyz,34.0549,-118.243 +relay.angor.io,48.1046,11.6002 +conduitl2.fly.dev,38.7946,-106.535 +relay2.angor.io,48.1046,11.6002 +relay.getsafebox.app,43.6532,-79.3832 +nostrcity-club.fly.dev:443,38.7946,-106.535 +vault.iris.to,43.6532,-79.3832 +nostr-01.uid.ovh,50.9871,2.12554 +nostr.sathoarder.com:443,48.5734,7.75211 +reraw.pbla2fish.cc,43.6532,-79.3832 +nostr.pbfs.io,50.4754,12.3683 +relay.mypathtofire.de,42.8864,-78.8784 +relay.nostrian-conquest.com,41.223,-111.974 +relay.nostriot.com,41.5695,-83.9786 +nostr.robosats.org,64.1476,-21.9392 +relay.getsafebox.app:443,43.6532,-79.3832 +dev.relay.stream,43.6532,-79.3832 +relay.btcforplebs.com,43.6532,-79.3832 +schnorr.me:443,43.6532,-79.3832 +nostrue.com,40.8054,-74.0241 +relay.aarpia.com,37.3986,-121.964 +relay.mccormick.cx:443,52.3563,4.95714 +relay.fiatdenier.com,43.7787,-79.5393 +freelay.sovbit.host,60.1699,24.9384 +nostr.computingcache.com,45.5341,-122.956 +relay.piazza.today,48.122,11.589 +relay.nostr.net,43.6532,-79.3832 +nostr-relay.xbytez.io,50.6924,3.20113 +strfry.shock.network:443,39.0438,-77.4874 +nostr.girino.org,43.6532,-79.3832 +nostr-pub.wellorder.net,45.5201,-122.99 +nostr.hifish.org,47.4009,8.57814 +relay.ezernode.net,52.6921,6.19372 +relay.artiostr.ch,43.6532,-79.3832 +relay01.lnfi.network,35.6764,139.65 +relay.edufeed.org:443,49.4521,11.0767 +relay.loveisbitcoin.com,43.6532,-79.3832 +nostr.purpura.cloud,43.6532,-79.3832 +relay.atsocy.com,43.6532,-79.3832 +relay.conduit.market,38.7946,-106.535 +relay.pocketnostr.com,40.8054,-74.0241 +relay.cosmicbolt.net,37.3986,-121.964 +relay.bornheimer.app,50.1109,8.68213 +nostrcity-club.fly.dev,38.7946,-106.535 +relay.nostr.dev.br,48.1046,11.6002 +relay.nuts.cash,52.3676,4.90414 +nostr.rtvslawenia.com,49.4543,11.0746 +relay.tdw.lol,41.8781,-87.6298 +relay.ohstr.com,43.6532,-79.3832 +relay.chatbett.de,40.7128,-74.006 +nostr2.girino.org:443,43.6532,-79.3832 +relay.dwadziesciajeden.pl,52.2297,21.0122 +nostr.spicyz.io,43.6532,-79.3832 +relay2.fiatdenier.com,50.1013,8.62643 +relay.binaryrobot.com:443,43.6532,-79.3832 +nostr.thebiglake.org,32.71,-96.6745 +relay.jmoose.rocks,32.8009,-96.8195 +nostr.easycryptosend.it,43.6532,-79.3832 +relay.aidatanorge.no,43.6532,-79.3832 +memlay.v0l.io,53.3498,-6.26031 +dm-test-nostr-rs-42-disabled.samt.st,43.6532,-79.3832 +nostr.notribe.net,40.8302,-74.1299 +relay.plebeian.market,50.1109,8.68213 +nostr.islandarea.net:443,35.4669,-97.6473 +ensemble-appear-locking-forming.trycloudflare.com,43.6532,-79.3832 +nostr.bitcoiner.social,47.6743,-117.112 +relay.samt.st,40.8302,-74.1299 +nostr-relay.corb.net,39.6478,-104.988 +relay.tdw.gg,34.0549,-118.243 +relay.olas.app,60.1699,24.9384 +relay.yoinekodo.jp,43.6532,-79.3832 +relay.mccormick.cx,52.3563,4.95714 +syb.lol,34.0549,-118.243 +0x-nostr-relay.fly.dev,38.7946,-106.535 +vm-1734.lnvps.cloud,53.3498,-6.26031 +relay.sigit.io:443,50.4754,12.3683 +node.kommonzenze.de,49.4521,11.0767 +staging.yabu.me,35.6092,139.73 +strfry.bonsai.com,39.0438,-77.4874 +relay.44billion.net,43.6532,-79.3832 +nostr.mom,50.4754,12.3683 +relay.notoshi.win,13.3622,100.983 +nostr.ltd,19.076,72.8777 +relay.fountain.fm:443,43.6532,-79.3832 +bruh.samt.st,43.6532,-79.3832 +relay.mitosystem.net,-23.5558,-46.6396 +nostr.myshosholoza.co.za,52.3676,4.90414 +nostr.fullstackcash.net,45.5201,-122.99 +nostr.na.social,43.6532,-79.3832 +nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 +nostr.88mph.life,52.1941,-2.21905 +relay.qstr.app,50.1109,8.68213 +relay.lightning.pub,39.0438,-77.4874 +relay.orangesync.tech,40.7128,-74.006 +nostr-02.yakihonne.com,1.32123,103.695 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +relay.bnos.space,43.6532,-79.3832 +temp.iris.to,43.6532,-79.3832 +nostr.relay.hedwig.sh,60.1699,24.9384 +relay.nostr.space,43.6532,-79.3832 +nostr.nestoris.online,55.568,37.5853 +yabu.me,35.6092,139.73 +nostr.wild-vibes.ts.net,48.8566,2.35222 +bcast.seutoba.com.br,43.6532,-79.3832 +nostr-dev.wellorder.net,45.5201,-122.99 +portal-relay.pareto.space,49.4521,11.0767 +hasky.chat,40.7862,-74.0743 +relay.lanavault.space,60.1699,24.9384 +offchain.pub,39.1585,-94.5728 +nostr-relay.cbrx.io,43.6532,-79.3832 +relay.nostrmap.net:443,60.1699,24.9384 +relay.wavlake.com,41.2619,-95.8608 +bitchat.nostr1.com,40.7057,-74.0136 +relay.mappingbitcoin.com,43.6532,-79.3832 +relay.ru.ac.th,13.7607,100.627 +relay.nostr.com,50.1109,8.68213 +relay.noeudlibre.fr,50.6924,3.20113 +relay.satmaxt.xyz:443,43.6532,-79.3832 +relay-dev.gulugulu.moe,43.6532,-79.3832 +no.str.cr:443,8.96171,-83.5246 +relay.nostrmap.net,60.1699,24.9384 +relay.littlebitstudios.com,43.6532,-79.3832 +relay-na1.metanomalist.com,43.6532,-79.3832 bucket.coracle.social,37.7775,-122.397 +relay.nostr.bond,40.1885,29.061 +relay.nostx.io,43.6532,-79.3832 +relay.nostrfeed.com,60.1699,24.9384 +relay.wavlake.com:443,41.2619,-95.8608 +social.amanah.eblessing.co,48.1046,11.6002 +relay-dev.gulugulu.moe:443,43.6532,-79.3832 +relay.kilombino.com,43.6532,-79.3832 +relay.lightning.pub:443,39.0438,-77.4874 +relay.bitmacro.cloud,43.6532,-79.3832 +relay.solomonstr.com,43.6532,-79.3832 +relay.agentry.com,42.8864,-78.8784 +relay.decentralia.fr,48.122,11.589 +articles.layer3.news:443,37.3387,-121.885 +nostr.tagomago.me,42.3601,-71.0589 +relay.illuminodes.com,43.6532,-79.3832 +nostr.nodesmap.com,59.3327,18.0656 +relay.scuba323.com,40.8218,-74.45 +nostr.yutakobayashi.com,43.6532,-79.3832 +relay.pyramid.li,47.4093,8.46503 +testr.nymble.world,40.8054,-74.0241 +relay.mostr.pub,43.6532,-79.3832 +infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 +relay.artio.inf.unibe.ch,46.9501,7.43678 +relay-can.zombi.cloudrodion.com,43.6532,-79.3832 +relay.pprgb.app,52.52,13.405 +fanfares.nostr1.com:443,40.7057,-74.0136 +relay.lanacoin-eternity.com:443,40.8302,-74.1299 +support.flotilla.social,32.9483,-96.7299 +relay.agora.social,50.7383,15.0648 +relay-us.zombi.cloudrodion.com,40.7862,-74.0743 +offchain.pub:443,39.1585,-94.5728 +nostr.snowbla.de,50.4754,12.3683 +nostr.tabordalab.com,60.1699,24.9384 +relay.nostrhub.fr,48.1045,11.6004 +relay.chorus.community,50.7529,6.84585 +nostr.linky.fit,50.1109,8.68213 +relay.islandbitcoin.com,20.3898,78.0903 +nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 +relay.orly.dev,32.7767,-96.797 relay.chorus.community:443,50.7529,6.84585 +relay.sigit.io,50.4754,12.3683 +relayone.geektank.ai,39.0997,-94.5786 +nostr.chaima.info,51.5072,-0.127586 +borgar.lol,52.3676,4.90414 +dm-test-strfry-discovery.samt.st,43.6532,-79.3832 +relay.plebeian.market:443,50.1109,8.68213 relay.hivetalk.org,40.8302,-74.1299 relay.favillakey.io,-27.4705,153.026 -nostr.mom,50.4754,12.3683 -relay2.orangesync.tech,40.7128,-74.006 -relay.artiostr.ch,43.6532,-79.3832 -relay.lacrypta.ar,43.6532,-79.3832 -treuzkas.branruz.com,48.8575,2.35138 -relay2.veganostr.com,60.1699,24.9384 -borgar.lol,52.3676,4.90414 -relay.ltgnet.work:8443,51.0511,-114.075 -relay.nostrmap.net,60.1699,24.9384 -nostr.mom:443,50.4754,12.3683 -relay.decentralia.fr,48.122,11.589 -relay.atsocy.com,43.6532,-79.3832 -strfry.shock.network:443,39.0438,-77.4874 -relay.vrtmrz.net,43.6532,-79.3832 -0x-nostr-relay.fly.dev,38.7946,-106.535 -relay.veganostr.com,60.1699,24.9384 -relay.nostr.bond,40.1885,29.061 -relay.angor.io,48.1046,11.6002 -nostr.sathoarder.com:443,48.5734,7.75211 -nostr.oxtr.dev,50.4754,12.3683 -dev.relay.stream,43.6532,-79.3832 -relay.mitchelltribe.com,39.0438,-77.4874 -nostr-relay.cbrx.io,43.6532,-79.3832 -nostr2.girino.org:443,43.6532,-79.3832 -relay.snotr.nl:49999,51.9758,4.31389 -n.musicroadmap.com,35.694,139.754 -relay.loveisbitcoin.com,43.6532,-79.3832 -relay.conduit.market,38.7946,-106.535 -schnorr.me:443,43.6532,-79.3832 -nostr-kyomu-haskell.onrender.com,37.7775,-122.397 -nostr.dlcdevkit.com,40.0992,-83.1141 -relay2.angor.io,48.1046,11.6002 -nostr.hifish.org,47.4009,8.57814 -rele.speyhard.fi,51.5072,-0.127586 -nostr.chaima.info:443,51.5072,-0.127586 -relay.trotters.cc:443,43.6532,-79.3832 -nostrmxn.lulus.com.mx,59.4016,17.9455 -chat-relay.zap-work.com:443,43.6532,-79.3832 -relay.tdw.gg,34.0549,-118.243 -relay.ohstr.com,43.6532,-79.3832 -relay.guggero.org,46.5971,9.59652 -nostr.stakey.net:443,52.3676,4.90414 -dev-relay.nostr.space,43.6532,-79.3832 -nostr.snowbla.de,50.4754,12.3683 -relay.piazza.today,48.122,11.589 -relay.fountain.fm:443,43.6532,-79.3832 -relay-dev.gulugulu.moe,43.6532,-79.3832 -relay.ditto.pub:443,43.6532,-79.3832 -vm-1734.lnvps.cloud,53.3498,-6.26031 -relay.beginningend.com,35.2227,-97.4786 -nos.lol,50.4754,12.3683 -nostr-01.yakihonne.com:443,1.32123,103.695 -relay.sigit.io:443,50.4754,12.3683 -nostr.janx.com,43.6532,-79.3832 -relay.wavlake.com,41.2619,-95.8608 -relayrs.notoshi.win:443,43.6532,-79.3832 -openrelay.ziomc.com,45.4642,9.18998 -nostr.88mph.life,52.1941,-2.21905 -maxq.descendant.io,43.6532,-79.3832 -nostr.wecsats.io:443,43.6532,-79.3832 -relay.bitmacro.cloud,43.6532,-79.3832 -relay.lightning.pub,39.0438,-77.4874 -strfry.bonsai.com,39.0438,-77.4874 -chat-relay.zap-work.com,43.6532,-79.3832 -relay.illuminodes.com,43.6532,-79.3832 -nostr.thebiglake.org,32.71,-96.6745 -ribo.eu.nostria.app,43.6532,-79.3832 -relay.novospes.com,43.6532,-79.3832 -nostr.notribe.net,40.8302,-74.1299 -nostr-relay.corb.net:443,39.6478,-104.988 -relay.pprgb.app,52.52,13.405 -relay.wellorder.net,45.5201,-122.99 -relay.mitchelltribe.com:443,39.0438,-77.4874 -cyberspace.nostr1.com,40.7057,-74.0136 -bruh.samt.st,43.6532,-79.3832 -nostr.davenov.com,50.1109,8.68213 -nostr.sovereignservices.xyz,43.6532,-79.3832 -relay-dev.gulugulu.moe:443,43.6532,-79.3832 -nostr.unkn0wn.world,46.8499,9.53287 -relay.nostr.space,43.6532,-79.3832 -infinity-signal-relay.digitalforlifeagency.workers.dev,43.6532,-79.3832 -relay.ezernode.net,52.6921,6.19372 -no.str.cr,8.96171,-83.5246 -spamspamspamspam.rest,43.6532,-79.3832 -relay.ru.ac.th,13.7607,100.627 -relay.tdw.lol,41.8781,-87.6298 -buzz.cashu.space,50.1109,8.68213 -relay.duck1123.com,43.6532,-79.3832 -relay.tapestry.ninja,40.8054,-74.0241 -relay.layer.systems,49.0291,8.35695 -relay.dyne.org,49.0291,8.35705 -relay.aidatanorge.no,43.6532,-79.3832 -relay.opmaat.org,60.1699,24.9384 -nostr.azzamo.net,52.2633,21.0283 -relay.mccormick.cx,52.3563,4.95714 -relay.nostr.com,50.1109,8.68213 -nostr.plantroon.com,50.1013,8.62643 -support.flotilla.social,32.9483,-96.7299 -relay.veganostr.com:443,60.1699,24.9384 -relay.notoshi.win,12.9333,100.883 -relay.edufeed.org,49.4521,11.0767 -memlay.v0l.io,53.3498,-6.26031 -bitcoiner.social,47.6743,-117.112 -relay.manneken.brussels,49.4543,11.0746 -nosflare-leefcore.leefcore.workers.dev,43.6532,-79.3832 -no.str.cr:443,8.96171,-83.5246 -relay.cosmicbolt.net,37.3986,-121.964 -relay.getsafebox.app:443,43.6532,-79.3832 -relay.aarpia.com,37.3986,-121.964 -nostr.sathoarder.com,48.5734,7.75211 -relay.getsafebox.app,43.6532,-79.3832 -nostr.chaima.info,51.5072,-0.127586 -relay.ditto.pub,43.6532,-79.3832 -nostr.4rs.nl,49.0291,8.35696 -rilo.nostria.app,43.6532,-79.3832 -nostr.christiansass.de,51.7883,6.13865 -relay.paulstephenborile.com:443,49.4543,11.0746 -relay.earthly.city,34.1749,-118.54 -inbox.scuba323.com,40.8218,-74.45 -nostr-01.yakihonne.com,1.32123,103.695 -articles.layer3.news:443,37.3387,-121.885 -offchain.pub,39.1585,-94.5728 -strfry.bonsai.com:443,39.0438,-77.4874 -relay.nostriot.com,41.5695,-83.9786 relay.bullishbounty.com,43.6532,-79.3832 -dm-test-strfry-generic.samt.st,43.6532,-79.3832 -relay.lanacoin-eternity.com:443,40.8302,-74.1299 -dm-test-strfry-discovery.samt.st,43.6532,-79.3832 -nostr.emanuelemiani.it,45.778,8.79414 -reraw.pbla2fish.cc,43.6532,-79.3832 -relay.s-w.art,43.6532,-79.3832 -relay.nostrcheck.me,43.6532,-79.3832 -relay.npubhaus.com,43.6532,-79.3832 -nostr.vulpem.com,49.4543,11.0746 -relay.wasabiwallet.io,43.6532,-79.3832 -nostr.liberty.fans,36.8767,-89.5879 -relay.paulstephenborile.com,49.4543,11.0746 -relay.nostrmap.net:443,60.1699,24.9384 -tribune-panel-growing-noon.trycloudflare.com,43.6532,-79.3832 -relay-na1.metanomalist.com,43.6532,-79.3832 -relay.agentry.com,42.8864,-78.8784 -relay.samt.st,40.8302,-74.1299 -relay.nostx.io,43.6532,-79.3832 -relay.cosmicbolt.net:443,37.3986,-121.964 -relayrs.notoshi.win,43.6532,-79.3832 +no.str.cr,8.96171,-83.5246 +relay.dyne.org,49.0291,8.35705 adre.su,59.9311,30.3609 -nostrcheck.me,43.6532,-79.3832 -relay-us.zombi.cloudrodion.com,40.7862,-74.0743 -nrs-01.darkcloudarcade.com,39.0997,-94.5786 +relay.mitchelltribe.com,39.0438,-77.4874 +relay.duck1123.com,43.6532,-79.3832 +relay.staging.plebeian.market,51.5072,-0.127586 +nostr.twinkle.lol,51.902,7.6657 +nostr.mom:443,50.4754,12.3683 +relay.jmoose.rocks:443,32.8009,-96.8195 +relay.satsmarkt.club,52.6907,4.8181 +relay.nostrcheck.me,43.6532,-79.3832 +cyberspace.nostr1.com,40.7057,-74.0136 +relay.veganostr.com:443,60.1699,24.9384 +relay.nearhood.co.uk,51.5134,-0.0890675 +nostr.stakey.net,52.3676,4.90414 +nostr-rs-relay.dev.fedibtc.com:443,39.0438,-77.4874 +relay.nostr-check.me,43.6532,-79.3832 +bcast.girino.org,43.6532,-79.3832 +nostr.wecsats.io:443,43.6532,-79.3832 +chat-relay.zap-work.com:443,43.6532,-79.3832 +nostr.azzamo.net,52.2633,21.0283 +relayone.soundhsa.com:443,39.0997,-94.5786 +pymd.kamp.site,38.9072,-77.0369 +relay.sharegap.net,43.6532,-79.3832 +relay.satmaxt.xyz,43.6532,-79.3832 relay.nmail.li,50.9871,2.12554 -relay.wisp.talk,49.4543,11.0746 -relay.mostr.pub,43.6532,-79.3832 -relay.jmoose.rocks,32.8009,-96.8195 -relay.laantungir.net,-19.4692,-42.5315 -nostr.nodesmap.com,59.3327,18.0656 -freelay.sovbit.host,60.1699,24.9384 -soloco.nl,43.6532,-79.3832 -ribo.nostria.app,43.6532,-79.3832 -relay.chatbett.de,40.7128,-74.006 -groups.beginningend.com,35.2227,-97.4786 -relay.chorus.community,50.7529,6.84585 -wisp.djorivaltech.com,43.6532,-79.3832 -nostr-02.yakihonne.com,1.32123,103.695 +fanfares.nostr1.com,40.7057,-74.0136 +relay-testnet.k8s.layer3.news,37.3387,-121.885 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +strfry.bonsai.com:443,39.0438,-77.4874 +public.crostr.com:443,43.6532,-79.3832 +nostrride.io,37.3986,-121.964 +relay.routstr.com,59.4016,17.9455 +public.crostr.com,43.6532,-79.3832 +relayone.geektank.ai:443,39.0997,-94.5786 +relay.kaleidoswap.com,50.8476,4.35717 +relay.stg.formstr.app,54.6872,25.2797 +relay.paulstephenborile.com,49.4543,11.0746 +nostr.data.haus:443,50.4754,12.3683 +nostriches.club,43.6532,-79.3832 +relayrs.notoshi.win,43.6532,-79.3832 +relay.bitos.space,43.6532,-79.3832 +relay.shadowbip.com,54.352,18.6466 +relay.bullishbounty.com:443,43.6532,-79.3832 +nostr.planix.org,43.6532,-79.3832 +relay.liberbitworld.org,43.6532,-79.3832 +nostr.sathoarder.com,48.5734,7.75211 +nostr.whitenode45.ddns.net,40.55,-74.4758 +relay.directsponsor.net,42.8864,-78.8784 +nostr.hekster.org,37.3986,-121.964 +relay-arg.zombi.cloudrodion.com,1.35208,103.82 +relay.degmods.com,50.4754,12.3683 +nostr.spaceshell.xyz,43.6532,-79.3832 +nostr.chaima.info:443,51.5072,-0.127586 +nostr.pbfs.io:443,50.4754,12.3683 +dm-test-strfry-generic.samt.st,43.6532,-79.3832 +spamspamspamspam.rest,43.6532,-79.3832 +nr.yay.so,46.2126,6.1154 +relay.minibolt.info,43.6532,-79.3832 +relay.mitchelltribe.com:443,39.0438,-77.4874 +nostr.janx.com,43.6532,-79.3832 +nostrelay.circum.space,52.2245,8.826 +relay.earthly.city,34.1749,-118.54 +budabit.nostr1.com,40.7057,-74.0136 +relay.underorion.se,50.1109,8.68213 +relay.paulstephenborile.com:443,49.4543,11.0746 +nostr-pr02.redscrypt.org,52.3676,4.90414 +relay.novospes.com,43.6532,-79.3832 +relay.keykeeper.world,40.7824,-74.0711 +relay2.orangesync.tech,40.7128,-74.006 +nostr-01.yakihonne.com:443,1.32123,103.695 +relay.trotters.cc:443,43.6532,-79.3832 +treuzkas.branruz.com,48.8575,2.35138 +nostr.21crypto.ch,47.5356,8.73209 +relay1.privkey.io,25.7617,-80.1918 +buzz.cashu.space,50.1109,8.68213 +chorus.mikedilger.com:444,-36.8906,174.794 +wheat.happytavern.co,43.6532,-79.3832 +nostr.stakey.net:443,52.3676,4.90414 +prl.plus,55.7558,37.6173 +relay.vrtmrz.net,43.6532,-79.3832 +relay.pocketstr.com,40.8302,-74.1299 +relay.snort.social,53.3498,-6.26031 +relay.flashapp.me,43.6548,-79.3885 +nostr.emanuelemiani.it,45.778,8.79414 +relay.wisp.talk:443,49.4543,11.0746 +vault.iris.to:443,43.6532,-79.3832 +relay.staging.plebeian.market:443,51.5072,-0.127586 +offchain.bostr.online,43.6532,-79.3832 +maxq.descendant.io,43.6532,-79.3832 +relay.plebchain.club,43.6532,-79.3832 +nostr.2b9t.xyz:443,34.0549,-118.243 +relay.ditto.pub:443,43.6532,-79.3832 +dev-relay.nostreon.com,60.1699,24.9384 +nostr.tbxnetworx.de,47.8226,10.0064 +nostr.4rs.nl,49.0291,8.35696 +nostr2.girino.org,43.6532,-79.3832 +relayrs.notoshi.win:443,43.6532,-79.3832 +relay.ltgnet.work:8443,51.0511,-114.075 +relay.fountain.fm,43.6532,-79.3832 +relay.wasabiwallet.io,43.6532,-79.3832 +relay.trotters.cc,43.6532,-79.3832 +nostr-relay.xbytez.io:443,50.6924,3.20113 +nostr.oxtr.dev:443,50.4754,12.3683