Compare commits

..

No commits in common. "main" and "v2.0.0" have entirely different histories.
main ... v2.0.0

29 changed files with 486 additions and 1385 deletions

View File

@ -1,156 +0,0 @@
# bitchat Privacy Policy
*Last updated: January 2025*
## Our Commitment
bitchat is designed with privacy as its foundation. We believe private communication is a fundamental human right. This policy explains how bitchat protects your privacy.
## Summary
**WE DO NOT COLLECT ANY INFORMATION.**
- **No personal data collection** - We don't collect names, emails, or phone numbers
- **No location data collection** - Location is accessed only for local processing (BLE/Geohash) and is never collected or sent to us
- **Hybrid Functionality** - bitchat offers two modes of communication:
- **Bluetooth Mesh Chat**: This mode is completely offline, using peer-to-peer Bluetooth connections. It does not use any servers or internet connection.
- **Geohash Chat**: This mode uses an internet connection to communicate with others in a specific geographic area. It relies on Nostr relays for message transport.
- **No tracking** - We have no analytics, telemetry, or user tracking
- **Open source** - You can verify these claims by reading our code
## What Information bitchat Stores
### On Your Device Only
1. **Identity Key**
- A cryptographic key generated on first launch
- Stored locally in your device's secure storage
- Allows you to maintain "favorite" relationships across app restarts
- Never leaves your device
2. **Nickname**
- The display name you choose (or auto-generated)
- Stored only on your device
- Shared with peers you communicate with
3. **Message History** (if enabled)
- When room owners enable retention, messages are saved locally
- Stored encrypted on your device
- You can delete this at any time
4. **Favorite Peers**
- Public keys of peers you mark as favorites
- Stored only on your device
- Allows you to recognize these peers in future sessions
### Temporary Session Data
During each session, bitchat temporarily maintains:
- Active peer connections (forgotten when app closes)
- Routing information for message delivery
- Cached messages for offline peers (12 hours max)
## What Information is Shared
### With Other bitchat Users
When you use bitchat, nearby peers can see:
- Your chosen nickname
- Your ephemeral public key (changes each session)
- Messages you send to public rooms or directly to them
- Your approximate Bluetooth signal strength (for connection quality)
### With Room Members
When you join a password-protected room:
- Your messages are visible to others with the password
- Your nickname appears in the member list
- Room owners can see you've joined
## What We DON'T Do
bitchat **never**:
- Collects personal information
- Collects location history
- Transmits any data to us (the developers)
- Stores data on servers
- Shares data with third parties
- Uses analytics or telemetry
- Creates user profiles
- Requires registration
## Encryption
All private messages use end-to-end encryption:
- **X25519** for key exchange
- **AES-256-GCM** for message encryption
- **Ed25519** for digital signatures
- **Argon2id** for password-protected rooms
## Your Rights
You have complete control:
- **Delete Everything**: Triple-tap the logo to instantly wipe all data
- **Leave Anytime**: Close the app and your presence disappears
- **No Account**: Nothing to delete from servers because there are none
- **Portability**: Your data never leaves your device unless you export it
## Location Data & Permissions
To provide the core functionality of bitchat, we access your device's location data. This access is necessary for the following specific purposes:
### 1. Bluetooth Low Energy (BLE) Scanning
- **Why we need it:** The Android operating system requires Location permission to scan for nearby Bluetooth LE devices (especially on Android 11 and lower). This is a system-level requirement because Bluetooth scans can theoretically be used to derive location.
- **How we use it:** We use this permission strictly to discover other bitchat peers nearby for the "Bluetooth Mesh Chat" mode.
- **Privacy protection:** We do not record or store your location during this process. The data is processed instantaneously by the Android system to facilitate the connection.
### 2. Geohash Chat Functionality
- **Why we need it:** The "Geohash Chat" mode allows you to communicate with others in your approximate geographic area.
- **How we use it:** If you enable this mode, we access your location to calculate a "geohash" (a short alphanumeric string representing a geographic region). This geohash is used to find and subscribe to relevant channels on decentralized Nostr relays.
- **Privacy protection:**
- Your precise GPS coordinates are **never** sent to any server or peer.
- Only the coarse geohash (representing an area, not a pinpoint) is shared with the Nostr network.
- You can use the "Bluetooth Mesh Chat" mode without this feature if you prefer.
**We do not collect, store, or share your location history.** Location data is processed locally on your device to enable these specific features.
## Children's Privacy
bitchat does not knowingly collect information from children. The app has no age verification because it collects no personal information from anyone.
## Data Retention
- **Messages**: Deleted from memory when app closes (unless room retention is enabled)
- **Identity Key**: Persists until you delete the app
- **Favorites**: Persist until you remove them or delete the app
- **Everything Else**: Exists only during active sessions
## Security Measures
- All communication is encrypted
- No data transmitted to servers (there are none)
- Open source code for public audit
- Regular security updates
- Cryptographic signatures prevent tampering
## Changes to This Policy
If we update this policy:
- The "Last updated" date will change
- The updated policy will be included in the app
- No retroactive changes can affect data (since we don't collect any)
## Contact
bitchat is an open source project. For privacy questions:
- Review our code: https://github.com/yourusername/bitchat
- Open an issue on GitHub
- Join the discussion in public rooms
## Philosophy
Privacy isn't just a feature—it's the entire point. bitchat proves that modern communication doesn't require surrendering your privacy. No accounts, no servers, no surveillance. Just people talking freely.
---
*This policy is released into the public domain under The Unlicense, just like bitchat itself.*

View File

@ -30,8 +30,8 @@ android {
applicationId = "com.bitchat.droid" applicationId = "com.bitchat.droid"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 38 versionCode = 37
versionName = "2.0.1" versionName = "2.0.0"
buildConfigField( buildConfigField(
"String", "String",
"GITHUB_RELEASE_CERT_SHA256", "GITHUB_RELEASE_CERT_SHA256",
@ -111,13 +111,6 @@ android {
} }
} }
composeCompiler {
// Kotlin 2.4.10's optional Compose group-key mapping depends on unspecified
// class-file iteration order. Keep the normal R8 mapping, but omit that
// augmentation until its producer is deterministic across clean builds.
includeComposeMappingFile.set(false)
}
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(21)
compilerOptions { compilerOptions {

View File

@ -1,400 +1,414 @@
Relay URL,Latitude,Longitude 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
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 nostr.n7ekb.net,47.4941,-122.294
purplerelay.com:443,43.6532,-79.3832 relay.kilombino.com,43.6532,-79.3832
nostr.tbxnetworx.de,47.7228,9.96456 relay02.lnfi.network,35.6764,139.65
relay.bnos.space:443,43.6532,-79.3832 relay.liberbitworld.org,43.6532,-79.3832
relay.satsapp.me,50.4754,12.3683 articles.layer3.news,37.3387,-121.885
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 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 nostr.easycryptosend.it,43.6532,-79.3832
x.kojira.io,43.6532,-79.3832 strfry.bonsai.com,39.0438,-77.4874
adre.su,59.9311,30.3609 relay.olas.app:443,60.1699,24.9384
offchain.bostr.online,43.6532,-79.3832 relay.sharegap.net,43.6532,-79.3832
relay.libernet.app,43.6532,-79.3832 relay.ditto.pub,43.6532,-79.3832
nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 nostrride.io,37.3986,-121.964
nostr.rtvslawenia.com,49.4543,11.0746 relay.favillakey.io,-27.4705,153.026
relay.layer.systems,49.0291,8.35695 nostr-relay.xbytez.io,50.6924,3.20113
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.nostrmap.net:443,60.1699,24.9384
relay.piazza.today,48.122,11.589 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.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
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
nostr.rtvslawenia.com,49.4543,11.0746
relay.wavefunc.live:443,41.8781,-87.6298
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
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
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
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
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.orly.dev,32.7767,-96.797
nostrelay.circum.space:443,52.2245,8.826
nostr.notribe.net,40.8302,-74.1299

1 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
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
2 nostr.n7ekb.net 47.4941 -122.294
3 purplerelay.com:443 relay.kilombino.com 43.6532 -79.3832
4 nostr.tbxnetworx.de relay02.lnfi.network 47.7228 35.6764 9.96456 139.65
5 relay.bnos.space:443 relay.liberbitworld.org 43.6532 -79.3832
6 relay.satsapp.me articles.layer3.news 50.4754 37.3387 12.3683 -121.885
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
7 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
8 nostr.easycryptosend.it 43.6532 -79.3832
9 x.kojira.io strfry.bonsai.com 43.6532 39.0438 -79.3832 -77.4874
10 adre.su relay.olas.app:443 59.9311 60.1699 30.3609 24.9384
11 offchain.bostr.online relay.sharegap.net 43.6532 -79.3832
12 relay.libernet.app relay.ditto.pub 43.6532 -79.3832
13 nostr-rs-relay-ishosta.phamthanh.me nostrride.io 43.6532 37.3986 -79.3832 -121.964
14 nostr.rtvslawenia.com relay.favillakey.io 49.4543 -27.4705 11.0746 153.026
15 relay.layer.systems nostr-relay.xbytez.io 49.0291 50.6924 8.35695 3.20113
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
16 relay.nostrmap.net:443 60.1699 24.9384
17 relay.piazza.today mostro-p2p.tech 48.122 50.1109 11.589 8.68213
18 relay.aidatanorge.no 43.6532 -79.3832
19 nostr.openhoofd.nl 51.5717 3.70417
20 relay.chatbett.de 40.7128 -74.006
21 relay.ohstr.com:443 43.6532 -79.3832
22 nostr.overmind.lol:443 43.6532 -79.3832
23 nostr.red5d.dev 43.6532 -79.3832
24 relay.snort.social 53.3498 -6.26031
25 relay.directsponsor.net 42.8864 -78.8784
26 relay.bullishbounty.com:443 43.6532 -79.3832
27 relay.scuba323.com 40.8218 -74.45
28 nostr.spicyz.io 43.6532 -79.3832
29 nostr.chaima.info 50.1109 8.68213
30 nostr.purpura.cloud 43.6532 -79.3832
31 relay.nostrops.com 32.71 -96.6745
32 relay.veganostr.com:443 60.1699 24.9384
33 relay.ohstr.com 43.6532 -79.3832
34 soloco.nl 43.6532 -79.3832
35 nostr.plantroon.com:443 50.1013 8.62643
36 relay.chorus.community:443 48.5333 10.7
37 relay.fiatdenier.com 43.7787 -79.5393
38 nostr.whitenode45.ddns.net 40.55 -74.4758
39 relay.cosmicbolt.net:443 37.3986 -121.964
40 relay.lanacoin-eternity.com:443 40.8302 -74.1299
41 relay.cosmicbolt.net 37.3986 -121.964
42 fanfares.nostr1.com:443 40.7057 -74.0136
43 nostr-kyomu-haskell.onrender.com 37.7775 -122.397
44 relay.keykeeper.world 40.7824 -74.0711
45 relay-testnet.k8s.layer3.news 37.3387 -121.885
46 dm-test-strfry-generic.samt.st 43.6532 -79.3832
47 relay.laantungir.net -19.4692 -42.5315
48 nostr.sathoarder.com 48.5734 7.75211
49 relay.mccormick.cx:443 52.3563 4.95714
50 relay.solife.me 43.6532 -79.3832
51 relay.fountain.fm:443 43.6532 -79.3832
52 nostrcity-club.fly.dev 38.7946 -106.535
53 nostr.mas-family.eu 60.3478 15.7505
54 no.str.cr 8.96171 -83.5246
55 relay.wavefunc.live 41.8781 -87.6298
56 relay.cyberguy.fyi 52.6907 4.8181
57 adre.su 59.9311 30.3609
58 relay.erybody.com 41.4513 -81.7021
59 relay.mrmave.work 43.6532 -79.3832
60 relay.decentralia.fr 48.122 11.589
61 relay.mccormick.cx 52.3563 4.95714
62 nostr.dlcdevkit.com:443 40.0992 -83.1141
63 nostr-02.yakihonne.com 1.32123 103.695
64 nostr.21crypto.ch 47.5356 8.73209
65 slick.mjex.me 39.0418 -77.4744
66 nostr.spicyz.io:443 43.6532 -79.3832
67 relay-dev.gulugulu.moe 43.6532 -79.3832
68 relay.nostx.io 43.6532 -79.3832
69 relay.wisp.talk 49.4543 11.0746
70 relay.plebeian.market:443 50.1109 8.68213
71 relay.mostr.pub:443 43.6532 -79.3832
72 relay.aarpia.com 37.3986 -121.964
73 chat-relay.zap-work.com 43.6532 -79.3832
74 chat-relay.zap-work.com:443 43.6532 -79.3832
75 relay.mitchelltribe.com:443 39.0438 -77.4874
76 relay.paulstephenborile.com 49.4543 11.0746
77 relay.trotters.cc 43.6532 -79.3832
78 nexus.libernet.app:443 43.6532 -79.3832
79 syb.lol 34.0549 -118.243
80 relay.cypherflow.ai 48.8575 2.35138
81 relay.despera.space 52.1782 20.9926
82 relay.loveisbitcoin.com 43.6532 -79.3832
83 relay.rushmi0.win 43.6532 -79.3832
84 relay.degmods.com 50.4754 12.3683
85 relay.satlantis.io 39.0438 -77.4874
86 relay.underorion.se 50.1109 8.68213
87 nostr.mom:443 50.4754 12.3683
88 nostr-verified.wellorder.net 45.5201 -122.99
89 relay.nostreon.com 60.1699 24.9384
90 nostr.christiansass.de 54.3213 10.1349
91 rilo.nostria.app:443 43.6532 -79.3832
92 relay.trotters.cc:443 43.6532 -79.3832
93 kotukonostr.onrender.com 37.7775 -122.397
94 relay.nmail.li 50.9871 2.12554
95 relay.artio.inf.unibe.ch 46.9501 7.43678
96 nos.lol 50.4754 12.3683
97 nostr.snowbla.de:443 50.4754 12.3683
98 relay.openresist.com 43.6532 -79.3832
99 ribo.us.nostria.app 43.6532 -79.3832
100 bitchat.nostr1.com 40.7057 -74.0136
101 vm-1734.lnvps.cloud 53.3498 -6.26031
102 nostr-relay.xbytez.io:443 50.6924 3.20113
103 nostr.sathoarder.com:443 48.5734 7.75211
104 bcast.girino.org 43.6532 -79.3832
105 ribo.us.nostria.app:443 43.6532 -79.3832
106 strfry.shock.network 39.0438 -77.4874
107 relay.staging.plebeian.market:443 51.5072 -0.127586
108 offchain.pub:443 39.1585 -94.5728
109 relayone.soundhsa.com 39.0997 -94.5786
110 relay.libernet.app 43.6532 -79.3832
111 relay.earthly.city 34.1749 -118.54
112 relayrs.notoshi.win:443 43.6532 -79.3832
113 nostr.vulpem.com 49.4543 11.0746
114 relay.getsafebox.app:443 43.6532 -79.3832
115 relay.paulstephenborile.com:443 49.4543 11.0746
116 nostr.unkn0wn.world 46.8499 9.53287
117 relay.binaryrobot.com 43.6532 -79.3832
118 relay.wellorder.net 45.5201 -122.99
119 relayone.geektank.ai:443 39.0997 -94.5786
120 relay.wasabiwallet.io 43.6532 -79.3832
121 top.testrelay.top 43.6532 -79.3832
122 nostr.2b9t.xyz 34.0549 -118.243
123 relay.nostr.com 50.1109 8.68213
124 nostr.azzamo.net:443 52.2633 21.0283
125 nostr.dlcdevkit.com 40.0992 -83.1141
126 nostr.robosats.org 64.1476 -21.9392
127 nostr.iskarion.ddns.net 43.3076 -2.95421
128 relay2.angor.io 48.1046 11.6002
129 relay.mmwaves.de 48.8575 2.35138
130 relay.agorist.space:443 52.3734 4.89406
131 relay.dreamith.to 43.6532 -79.3832
132 nostr-02.uid.ovh 50.9871 2.12554
133 nostr.thebiglake.org 32.71 -96.6745
134 nostr-relay.corb.net 39.6478 -104.988
135 relay.dyne.org 49.0291 8.35705
136 nosflare-leefcore.leefcore.workers.dev 43.6532 -79.3832
137 relay.angor.io 48.1046 11.6002
138 nostrelay.circum.space 52.2245 8.826
139 nostr-01.uid.ovh 50.9871 2.12554
140 maxq.descendant.io 43.6532 -79.3832
141 offchain.bostr.online 43.6532 -79.3832
142 relay.binaryrobot.com:443 43.6532 -79.3832
143 nostr.planix.org 43.6532 -79.3832
144 tribune-panel-growing-noon.trycloudflare.com 43.6532 -79.3832
145 prl.plus 55.7628 37.5983
146 relay.mwaters.net 50.9871 2.12554
147 dev.relay.stream 43.6532 -79.3832
148 relay.bornheimer.app 50.1109 8.68213
149 inbox.scuba323.com 40.8218 -74.45
150 relay-us.zombi.cloudrodion.com 40.7862 -74.0743
151 relay.nuts.cash 52.3676 4.90414
152 bruh.samt.st 43.6532 -79.3832
153 nostr.carroarmato0.be:443 50.914 3.21378
154 relay.plebeian.market 50.1109 8.68213
155 schnorr.me:443 43.6532 -79.3832
156 nostr.thalheim.io:443 60.1699 24.9384
157 nostr.88mph.life 52.1941 -2.21905
158 cache.trustr.ing 43.6548 -79.3885
159 ec2.f7z.io 60.1699 24.9384
160 relay.btcforplebs.com 43.6532 -79.3832
161 nostr.liberty.fans 36.8767 -89.5879
162 syb.lol:443 34.0549 -118.243
163 nostr.islandarea.net:443 35.4669 -97.6473
164 nostr.mom 50.4754 12.3683
165 nostr.data.haus:443 50.4754 12.3683
166 strfry.bonsai.com:443 39.0438 -77.4874
167 nostr.highway15.net 28.0445 -82.6699
168 relay.wavlake.com:443 41.2619 -95.8608
169 relay.nostriot.com:443 41.5695 -83.9786
170 relay.gulugulu.moe:443 43.6532 -79.3832
171 rele.speyhard.fi 50.1109 8.68213
172 nostr.hoppe-relay.it.com 42.8864 -78.8784
173 nostr2.girino.org:443 43.6532 -79.3832
174 nostr.oxtr.dev 50.4754 12.3683
175 nostr.bitcoiner.social:443 40.7608 -111.891
176 nostrrelay.taylorperron.com 45.5029 -73.5723
177 nostr-01.yakihonne.com:443 1.32123 103.695
178 nostr.tagomago.me 42.3601 -71.0589
179 relay.hivetalk.org 40.8302 -74.1299
180 relay.sigit.io:443 50.4754 12.3683
181 relay.openresist.com:443 43.6532 -79.3832
182 cs-relay.nostrdev.com:443 50.4754 12.3683
183 no.str.cr:443 8.96171 -83.5246
184 relay.nearhood.co.uk 51.5134 -0.0890675
185 relay.lanavault.space 60.1699 24.9384
186 relay.guggero.org 46.5971 9.59652
187 nexus.libernet.app 43.6532 -79.3832
188 relay.nostriot.com 41.5695 -83.9786
189 dm-test-strfry-discovery.samt.st:443 43.6532 -79.3832
190 x.kojira.io:443 43.6532 -79.3832
191 nostr.4rs.nl 49.0291 8.35696
192 relayone.geektank.ai 39.0997 -94.5786
193 nostr.debate.report 50.1109 8.68213
194 relay.gulugulu.moe 43.6532 -79.3832
195 public.crostr.com 43.6532 -79.3832
196 offchain.pub 39.1585 -94.5728
197 relay.tdw.lol 41.8781 -87.6298
198 relay5.bitransfer.org 43.6532 -79.3832
199 relay.manneken.brussels 49.4543 11.0746
200 chorus.pjv.me 45.5201 -122.99
201 relay.fckstate.net 59.3293 18.0686
202 treuzkas.branruz.com 48.8575 2.35138
203 nostr-relay.cbrx.io 43.6532 -79.3832
204 nostr-relay.nextblockvending.com 47.2343 -119.853
205 nostr.fullstackcash.net 45.5201 -122.99
206 relay.primal.net 43.6532 -79.3832
207 relay.mappingbitcoin.com 43.6532 -79.3832
208 nostr.mikoshi.de 50.1109 8.68213
209 nostr.stakey.net 52.3676 4.90414
210 nostr-rs-relay-qj1h.onrender.com 37.7775 -122.397
211 relay.wavlake.com 41.2619 -95.8608
212 relay.ordoplay.com 50.1109 8.68213
213 relay.klabo.world 47.674 -122.122
214 buzz.cashu.space 50.1109 8.68213
215 relay.nostrhub.fr 48.1045 11.6004
216 portal-relay.pareto.space 49.0291 8.35696
217 dev-relay.nostreon.com 60.1699 24.9384
218 relay.islandbitcoin.com 12.8498 77.6545
219 testr.nymble.world 40.8054 -74.0241
220 relayrs.notoshi.win 43.6532 -79.3832
221 nostr-relay.corb.net:443 39.6478 -104.988
222 cdn.satellite.earth 40.8302 -74.1299
223 rilo.nostria.app 43.6532 -79.3832
224 nostr.pbfs.io 50.4754 12.3683
225 nostr2.girino.org 43.6532 -79.3832
226 0x-nostr-relay.fly.dev 38.7946 -106.535
227 nostr.girino.org 43.6532 -79.3832
228 relay.layer.systems 49.0291 8.35695
229 relay.olas.app 60.1699 24.9384
230 ribo.eu.nostria.app 43.6532 -79.3832
231 buzz.ac2n-share.kozow.com 45.764 4.83566
232 auth.nostr1.com 40.7057 -74.0136
233 relay.bitmacro.cloud 43.6532 -79.3832
234 relay.qstr.app 50.1109 8.68213
235 relay.ru.ac.th 13.7607 100.627
236 ribo.nostria.app:443 43.6532 -79.3832
237 relay.44billion.net 43.6532 -79.3832
238 cs-relay.nostrdev.com 50.4754 12.3683
239 relay.nostrian-conquest.com:443 41.223 -111.974
240 relay.bullishbounty.com 43.6532 -79.3832
241 relay.edufeed.org 49.4521 11.0767
242 porchlight.social 43.6532 -79.3832
243 nostr.bitcoiner.social 40.7608 -111.891
244 testnet-relay.samt.st 40.8302 -74.1299
245 relay.vrtmrz.net 43.6532 -79.3832
246 nostr.wecsats.io:443 43.6532 -79.3832
247 relay.satmaxt.xyz:443 43.6532 -79.3832
248 nostr.snowbla.de 50.4754 12.3683
249 nostr.rtvslawenia.com:443 49.4543 11.0746
250 relay.flashapp.me 43.6548 -79.3885
251 purplerelay.com:443 43.6532 -79.3832
252 relay.staging.plebeian.market 51.5072 -0.127586
253 relay.libernet.app:443 43.6532 -79.3832
254 relay.0xchat.com 43.6532 -79.3832
255 myvoiceourstory.org 37.3598 -121.981
256 nrs-01.darkcloudarcade.com:443 39.0997 -94.5786
257 relay.vrtmrz.net:443 43.6532 -79.3832
258 relay.zone667.com 60.1699 24.9384
259 nostr.nodesmap.com 59.3327 18.0656
260 relay.agorist.space 52.3734 4.89406
261 relayone.soundhsa.com:443 39.0997 -94.5786
262 relay.satmaxt.xyz 43.6532 -79.3832
263 relay.zapanimations.com 40.8054 -74.0241
264 relay.sovereignresonance.org 48.8575 2.35138
265 nostr.relay.hedwig.sh 60.1699 24.9384
266 relay-dev.gulugulu.moe:443 43.6532 -79.3832
267 relay.arx-ccn.com 50.4754 12.3683
268 nip85.nosfabrica.com 39.0997 -94.5786
269 nrs-01.darkcloudarcade.com 39.0997 -94.5786
270 spamspamspamspam.rest 43.6532 -79.3832
271 nostr.davenov.com 50.1109 8.68213
272 relay.littlebitstudios.com 43.6532 -79.3832
273 purplerelay.com 43.6532 -79.3832
274 nos.lol:443 50.4754 12.3683
275 relay2.veganostr.com 60.1699 24.9384
276 relay-rpi.edufeed.org 49.4521 11.0767
277 relay.lacrypta.ar 43.6532 -79.3832
278 relay.tdw.gg 34.0549 -118.243
279 relay.getvia.xyz 60.1699 24.9384
280 relay.lanacoin-eternity.com 40.8302 -74.1299
281 conduitl2.fly.dev 38.7946 -106.535
282 nostr.rtvslawenia.com 49.4543 11.0746
283 relay.wavefunc.live:443 41.8781 -87.6298
284 nostr.carroarmato0.be 50.914 3.21378
285 nostr.pbfs.io:443 50.4754 12.3683
286 ribo.nostria.app 43.6532 -79.3832
287 nostr.sovereignservices.xyz 43.6532 -79.3832
288 relay.mostr.pub 43.6532 -79.3832
289 schnorr.me 43.6532 -79.3832
290 x.kojira.io 43.6532 -79.3832
291 public.obelisk.ar 43.6532 -79.3832
292 relay.angor.io:443 48.1046 11.6002
293 nostr.oxtr.dev:443 50.4754 12.3683
294 nostr.ltd 19.076 72.8777
295 nostr.tac.lol:443 47.4748 -122.273
296 nostr.twinkle.lol 51.902 7.6657
297 relay.s-w.art 43.6532 -79.3832
298 relay.openfarmtools.org 60.1699 24.9384
299 nostr.hekster.org 37.3986 -121.964
300 strfry.apps3.slidestr.net 40.4167 -3.70329
301 relay.mypathtofire.de 42.8864 -78.8784
302 nostr.wecsats.io 43.6532 -79.3832
303 nostr.computingcache.com 34.0356 -118.442
304 nostr.janx.com 43.6532 -79.3832
305 bread.nostrsms.com 40.8218 -74.45
306 relay.chorus.community 48.5333 10.7
307 testnet-relay.samt.st:443 40.8302 -74.1299
308 relay.kaleidoswap.com 50.8476 4.35717
309 yabu.me 35.6092 139.73
310 nostr-dev.wellorder.net 45.5201 -122.99
311 relay.layer.systems:443 49.0291 8.35695
312 relay-can.zombi.cloudrodion.com 43.6532 -79.3832
313 nostr-relay.zimage.com 34.282 -118.439
314 relay.fundstr.me 42.3601 -71.0589
315 nostr.stakey.net:443 52.3676 4.90414
316 relay.satsmarkt.club 52.6907 4.8181
317 relay.satsapp.me 50.4754 12.3683
318 bendernostur.duckdns.org:8443 50.1109 8.68213
319 relay.illuminodes.com 43.6532 -79.3832
320 nostr.chaima.info:443 50.1109 8.68213
321 nostr.hekster.org:443 37.3986 -121.964
322 nostr.yutakobayashi.com 43.6532 -79.3832
323 freelay.sovbit.host 60.1699 24.9384
324 insta-relay.apps3.slidestr.net 40.4167 -3.70329
325 infinity-signal-relay.digitalforlifeagency.workers.dev 43.6532 -79.3832
326 relay.internationalright-wing.org -22.4692 -48.9875
327 relay.mmwaves.de:443 48.8575 2.35138
328 public.crostr.com:443 43.6532 -79.3832
329 relay.nostrian-conquest.com 41.223 -111.974
330 relay-rpi.edufeed.org:443 49.4521 11.0767
331 relay2.angor.io:443 48.1046 11.6002
332 relay-fra.zombi.cloudrodion.com 48.8566 2.35222
333 bunk-test.feeds.relay.tools 38.6327 -90.1961
334 nostr-pr02.redscrypt.org 52.3676 4.90414
335 nostr.tac.lol 47.4748 -122.273
336 nostr.overpay.com 29.7449 -95.5343
337 relay.novospes.com 43.6532 -79.3832
338 nostrcity-club.fly.dev:443 38.7946 -106.535
339 relay01.lnfi.network 35.6764 139.65
340 relay.lightning.pub:443 39.0438 -77.4874
341 relay.pyramid.li 47.4093 8.46503
342 relay.nostu.be 40.4167 -3.70329
343 nostr-rs-relay.dev.fedibtc.com:443 39.0438 -77.4874
344 relay.endfiat.money:443 59.3327 18.0656
345 nostr.spaceshell.xyz 43.6532 -79.3832
346 21milionidinostr.duckdns.org 45.5192 9.08625
347 articles.layer3.news:443 37.3387 -121.885
348 wheat.happytavern.co 43.6532 -79.3832
349 nostr-rs-relay.dev.fedibtc.com 39.0438 -77.4874
350 nostr.na.social 43.6532 -79.3832
351 social.amanah.eblessing.co 48.1046 11.6002
352 reraw.pbla2fish.cc 43.6532 -79.3832
353 nostr.girino.org:443 43.6532 -79.3832
354 staging.yabu.me 35.6092 139.73
355 nostr.islandarea.net 35.4669 -97.6473
356 relay.endfiat.money 59.3327 18.0656
357 relay.mitchelltribe.com 39.0438 -77.4874
358 nostr.myshosholoza.co.za 52.3676 4.90414
359 relay.staging.commonshub.brussels 49.4543 11.0746
360 bridge.tagomago.me 42.3601 -71.0589
361 relay.agentry.com 42.8864 -78.8784
362 testnet.samt.st 43.6532 -79.3832
363 nostr.wild-vibes.ts.net 48.8566 2.35222
364 relay.getsafebox.app 43.6532 -79.3832
365 relay.notoshi.win 13.3396 100.93
366 bitcoinostr.duckdns.org 41.1976 1.11167
367 relay.sigit.io 50.4754 12.3683
368 bucket.coracle.social 37.7775 -122.397
369 dm-test-strfry-discovery.samt.st 43.6532 -79.3832
370 vault.iris.to:443 43.6532 -79.3832
371 relay.wisp.talk:443 49.4543 11.0746
372 relay.mostro.network 40.8302 -74.1299
373 nostr.l484.com 30.2672 -97.7431
374 relay.froth.zone 60.1699 24.9384
375 relay.beamhop.com 43.6532 -79.3832
376 relay.sincensura.org 43.6532 -79.3832
377 relay.ditto.pub:443 43.6532 -79.3832
378 nostr.azzamo.net 52.2633 21.0283
379 relay.edufeed.org:443 49.4521 11.0767
380 ribo.eu.nostria.app:443 43.6532 -79.3832
381 vault.iris.to 43.6532 -79.3832
382 nostr.plantroon.com 50.1013 8.62643
383 fanfares.nostr1.com 40.7057 -74.0136
384 budabit.nostr1.com 40.7057 -74.0136
385 relay.damus.io 43.6532 -79.3832
386 strfry.shock.network:443 39.0438 -77.4874
387 bitcoiner.social 40.7608 -111.891
388 basspistol.org 49.0291 8.35696
389 nostr.thalheim.io 60.1699 24.9384
390 relay.conduit.market 38.7946 -106.535
391 relay.yoinekodo.jp 43.6532 -79.3832
392 nostrbtc.com 43.6532 -79.3832
393 relay.lightning.pub 39.0438 -77.4874
394 nostr-01.yakihonne.com 1.32123 103.695
395 nostr-pub.wellorder.net 45.5201 -122.99
396 strfry.ymir.cloud 43.6532 -79.3832
397 relay.minibolt.info 43.6532 -79.3832
398 nostr.data.haus 50.4754 12.3683
399 relay.otrta.me 50.1109 8.68213
400 relay.samt.st 40.8302 -74.1299
401 relay.nostr.blockhenge.com 39.0438 -77.4874
402 relay.minibolt.info:443 43.6532 -79.3832
403 nostr.2b9t.xyz:443 34.0549 -118.243
404 dm-test-nostr-rs-42-disabled.samt.st 43.6532 -79.3832
405 relay.veganostr.com 60.1699 24.9384
406 relay.fountain.fm 43.6532 -79.3832
407 node.kommonzenze.de 49.4521 11.0767
408 relay-arg.zombi.cloudrodion.com 1.35208 103.82
409 nostr.myshosholoza.co.za:443 52.3676 4.90414
410 temp.iris.to 43.6532 -79.3832
411 relay.nostrmap.net 60.1699 24.9384
412 relay.orly.dev 32.7767 -96.797
413 nostrelay.circum.space:443 52.2245 8.826
414 nostr.notribe.net 40.8302 -74.1299

View File

@ -29,23 +29,9 @@ data class PeerCapabilities(val rawValue: Long) : Parcelable {
companion object { companion object {
val NONE = PeerCapabilities(0) 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. */ /** Noise-encrypted private BitchatFilePacket using payload type 0x20. */
val PRIVATE_MEDIA = PeerCapabilities(1L shl 8) 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. */ /** Capabilities implemented by this Android build. */
val LOCAL_SUPPORTED = PRIVATE_MEDIA val LOCAL_SUPPORTED = PRIVATE_MEDIA

View File

@ -3,11 +3,8 @@ package com.bitchat.android.nostr
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Date import java.util.Date
/** /**
@ -21,8 +18,7 @@ class GeohashMessageHandler(
private val repo: GeohashRepository, private val repo: GeohashRepository,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val dataManager: com.bitchat.android.ui.DataManager, private val dataManager: com.bitchat.android.ui.DataManager,
private val addChannelMessage: (String, BitchatMessage) -> Unit, private val addChannelMessage: (String, BitchatMessage) -> Unit
private val signatureVerificationDispatcher: CoroutineDispatcher = Dispatchers.Default
) { ) {
companion object { private const val TAG = "GeohashMessageHandler" } companion object { private const val TAG = "GeohashMessageHandler" }
@ -48,13 +44,6 @@ class GeohashMessageHandler(
if (event.kind != NostrKind.EPHEMERAL_EVENT && event.kind != NostrKind.GEOHASH_PRESENCE) return@launch 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) val tagGeo = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }?.getOrNull(1)
if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch if (tagGeo == null || !tagGeo.equals(subscribedGeohash, true)) return@launch
val hasValidSignature = withContext(signatureVerificationDispatcher) {
event.isValidSignature()
}
if (!hasValidSignature) {
Log.w(TAG, "Rejecting geohash event ${event.id.take(8)}... with invalid signature")
return@launch
}
if (dedupe(event.id)) return@launch if (dedupe(event.id)) return@launch
// PoW validation (if enabled) - apply to chat messages primarily // PoW validation (if enabled) - apply to chat messages primarily

View File

@ -416,11 +416,6 @@ class LocationNotesManager private constructor() {
Log.v(TAG, "Ignoring non-text-note event: kind=${event.kind}") Log.v(TAG, "Ignoring non-text-note event: kind=${event.kind}")
return return
} }
if (!event.isValidSignature()) {
Log.w(TAG, "Rejecting note ${event.id.take(8)}... with invalid signature")
return
}
// Check for geohash tag // Check for geohash tag
val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" } val geohashTag = event.tags.firstOrNull { it.size >= 2 && it[0] == "g" }

View File

@ -282,11 +282,6 @@ class NostrClient private constructor(private val context: Context) {
handler: (content: String, senderPubkey: String, nickname: String?, timestamp: Int) -> Unit handler: (content: String, senderPubkey: String, nickname: String?, timestamp: Int) -> Unit
) { ) {
try { 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 // Check Proof of Work validation for incoming geohash events
val powSettings = PoWPreferenceManager.getCurrentSettings() val powSettings = PoWPreferenceManager.getCurrentSettings()
if (powSettings.enabled && powSettings.difficulty > 0) { if (powSettings.enabled && powSettings.difficulty > 0) {

View File

@ -1,7 +1,6 @@
package com.bitchat.android.protocol package com.bitchat.android.protocol
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
@ -35,27 +34,6 @@ object SpecialRecipients {
val BROADCAST = ByteArray(8) { 0xFF.toByte() } // All 0xFF = broadcast 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 * Binary packet format - 100% backward compatible with iOS version
* *
@ -83,10 +61,7 @@ data class BitchatPacket(
val payload: ByteArray, val payload: ByteArray,
var signature: ByteArray? = null, // Changed from val to var for packet signing var signature: ByteArray? = null, // Changed from val to var for packet signing
var ttl: UByte, var ttl: UByte,
var route: List<ByteArray>? = null, // Optional source route: ordered list of peerIDs (8 bytes each), not including sender and final recipient var route: List<ByteArray>? = 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 { ) : Parcelable {
constructor( constructor(
@ -125,8 +100,7 @@ data class BitchatPacket(
payload = payload, payload = payload,
signature = null, // Remove signature for signing signature = null, // Remove signature for signing
route = route, 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) return BinaryProtocol.encode(unsignedPacket)
} }
@ -240,17 +214,7 @@ object BinaryProtocol {
var originalPayloadSize: Int? = null var originalPayloadSize: Int? = null
var isCompressed = false var isCompressed = false
// Re-encode of a decoded packet: reuse the originator's bytes (see WirePayload). if (CompressionUtil.shouldCompress(payload)) {
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 -> CompressionUtil.compress(payload)?.let { compressedPayload ->
originalPayloadSize = payload.size originalPayloadSize = payload.size
payload = compressedPayload payload = compressedPayload
@ -486,8 +450,6 @@ object BinaryProtocol {
} else null } else null
// Payload // Payload
// Kept so the packet can be re-encoded byte-identically (see WirePayload).
var receivedCompressed: ByteArray? = null
val payload = if (isCompressed) { val payload = if (isCompressed) {
val lengthFieldBytes = if (version >= 2u.toUByte()) 4 else 2 val lengthFieldBytes = if (version >= 2u.toUByte()) 4 else 2
if (payloadLength.toInt() < lengthFieldBytes) return null if (payloadLength.toInt() < lengthFieldBytes) return null
@ -526,8 +488,6 @@ object BinaryProtocol {
val expandedPayload = CompressionUtil.withDecompressionResources(resourceBytes) { val expandedPayload = CompressionUtil.withDecompressionResources(resourceBytes) {
val compressedPayload = ByteArray(compressedSize) val compressedPayload = ByteArray(compressedSize)
buffer.get(compressedPayload) buffer.get(compressedPayload)
// Captured here so the allocation stays inside the reservation.
receivedCompressed = compressedPayload
decompress(compressedPayload, originalSize) decompress(compressedPayload, originalSize)
} ?: return null } ?: return null
if (expandedPayload.size != originalSize) { if (expandedPayload.size != originalSize) {
@ -560,12 +520,7 @@ object BinaryProtocol {
payload = payload, payload = payload,
signature = signature, signature = signature,
ttl = ttl, ttl = ttl,
route = route, route = route
wirePayload = WirePayload(
bytes = receivedCompressed ?: payload,
compressed = receivedCompressed != null,
forPayload = payload
)
) )
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -317,7 +317,6 @@ class ConversationRepository internal constructor(
true true
} catch (error: Exception) { } catch (error: Exception) {
Log.e(TAG, "Unable to synchronously clear private conversations", error) Log.e(TAG, "Unable to synchronously clear private conversations", error)
database.destroyStorage()
_storeState.value = ConversationStoreState.Error( _storeState.value = ConversationStoreState.Error(
error.message ?: "Unable to erase conversations" error.message ?: "Unable to erase conversations"
) )
@ -1098,22 +1097,6 @@ internal class ConversationDatabase(
writableDatabase.rawQuery("PRAGMA incremental_vacuum", null).use { } 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<String> { fun pruneToRetentionLimits(): Set<String> {
val db = writableDatabase val db = writableDatabase
val orphanedMediaPaths = linkedSetOf<String>() val orphanedMediaPaths = linkedSetOf<String>()

View File

@ -1,6 +1,5 @@
package com.bitchat.android.ui package com.bitchat.android.ui
import com.bitchat.android.geohash.ChannelID
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.security.MessageDigest import java.security.MessageDigest
@ -17,8 +16,7 @@ class ChannelManager(
private val state: ChatState, private val state: ChatState,
private val messageManager: MessageManager, private val messageManager: MessageManager,
private val dataManager: DataManager, private val dataManager: DataManager,
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope
private val onSwitchToMeshLocation: () -> Unit = {}
) { ) {
// Channel encryption and security // Channel encryption and security
@ -111,8 +109,6 @@ class ChannelManager(
fun switchToChannel(channel: String?) { fun switchToChannel(channel: String?) {
state.setCurrentChannel(channel) state.setCurrentChannel(channel)
state.setSelectedPrivateChatPeer(null) state.setSelectedPrivateChatPeer(null)
state.setSelectedLocationChannel(ChannelID.Mesh)
onSwitchToMeshLocation()
// Clear unread count // Clear unread count
channel?.let { ch -> channel?.let { ch ->

View File

@ -140,17 +140,7 @@ class ChatViewModel(
private val conversationListPreferences = private val conversationListPreferences =
com.bitchat.android.services.ConversationListPreferences.getInstance(getApplication()) com.bitchat.android.services.ConversationListPreferences.getInstance(getApplication())
private val messageManager = MessageManager(state) private val messageManager = MessageManager(state)
private val channelManager = ChannelManager( private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
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 // Create Noise session delegate for clean dependency injection
private val noiseSessionDelegate = object : NoiseSessionDelegate { private val noiseSessionDelegate = object : NoiseSessionDelegate {

View File

@ -942,61 +942,6 @@ class BinaryProtocolTest {
assertTrue("Payload must match", payload.contentEquals(decoded.payload)) 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 * v2 compression bomb is rejected
* *

View File

@ -24,12 +24,11 @@ import org.robolectric.RobolectricTestRunner
import java.util.Date import java.util.Date
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class CommandProcessorTest { class CommandProcessorTest() {
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
@OptIn(ExperimentalCoroutinesApi::class)
@OptIn(ExperimentalCoroutinesApi::class) private val testDispatcher = UnconfinedTestDispatcher()
private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher)
private val testScope = TestScope(testDispatcher)
private val chatState = ChatState(scope = testScope) private val chatState = ChatState(scope = testScope)
private lateinit var commandProcessor: CommandProcessor private lateinit var commandProcessor: CommandProcessor
@ -64,11 +63,11 @@ class CommandProcessorTest {
val channel = "channel-1" val channel = "channel-1"
val result = commandProcessor.processCommand( val result = commandProcessor.processCommand(
command = "/j $channel", command = "/j $channel",
meshService = meshService, meshService = meshService,
myPeerID = "peer-id", myPeerID = "peer-id",
onSendMessage = { a, b, c -> { } }, onSendMessage = { a, b, c -> { } },
viewModel = null viewModel = null
) )
assertEquals(result, true) assertEquals(result, true)
@ -164,24 +163,6 @@ class CommandProcessorTest {
assertEquals(0, chatState.getMessagesValue().size) 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)
}
@Test @Test
fun `clearSuggestions hides the command suggestion popup`() { fun `clearSuggestions hides the command suggestion popup`() {
// Typing "/" opens the command popup. // Typing "/" opens the command popup.
@ -206,6 +187,5 @@ class CommandProcessorTest {
commandProcessor.clearSuggestions() commandProcessor.clearSuggestions()
assertFalse(chatState.getShowMentionSuggestionsValue()) assertFalse(chatState.getShowMentionSuggestionsValue())
assertTrue(chatState.getMentionSuggestionsValue().isEmpty()) assertTrue(chatState.getMentionSuggestionsValue().isEmpty())
} }
} }

View File

@ -12,38 +12,9 @@ class IdentityAnnouncementTest {
private val signingKey = ByteArray(32) { 0x22 } private val signingKey = ByteArray(32) { 0x22 }
@Test @Test
fun `capability assignments and encoding match iOS`() { fun `private media capability uses iOS little-endian bytes`() {
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()) 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 @Test
@ -100,6 +71,5 @@ class IdentityAnnouncementTest {
.capabilities!! .capabilities!!
.contains(PeerCapabilities.PRIVATE_MEDIA) .contains(PeerCapabilities.PRIVATE_MEDIA)
) )
assertEquals(PeerCapabilities.PRIVATE_MEDIA, PeerCapabilities.LOCAL_SUPPORTED)
} }
} }

View File

@ -1,136 +0,0 @@
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 = application,
repo = repo,
scope = testScope,
dataManager = dataManager,
addChannelMessage = messageManager::addChannelMessage,
signatureVerificationDispatcher = testDispatcher
)
}
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)
}
@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)
}
}

View File

@ -36,21 +36,17 @@ class NostrBackgroundEventProcessorTest {
fun `cold start processes more events than the removed handoff queue capacity`() = runBlocking { fun `cold start processes more events than the removed handoff queue capacity`() = runBlocking {
val application = ApplicationProvider.getApplicationContext<Application>() val application = ApplicationProvider.getApplicationContext<Application>()
val processor = NostrBackgroundEventProcessor(application, scope) val processor = NostrBackgroundEventProcessor(application, scope)
val identity = NostrIdentity.generate()
val expectedIds = mutableSetOf<String>()
repeat(300) { index -> 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( processor.onGeohashMessage(
event = event, 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"
),
geohash = "u4pruy" geohash = "u4pruy"
) )
} }
@ -62,7 +58,7 @@ class NostrBackgroundEventProcessorTest {
} }
assertEquals( assertEquals(
expectedIds, (0 until 300).map { "cold-start-$it" }.toSet(),
AppStateStore.channelMessages.value["geo:u4pruy"].orEmpty().map { it.id }.toSet() AppStateStore.channelMessages.value["geo:u4pruy"].orEmpty().map { it.id }.toSet()
) )
} }

View File

@ -433,22 +433,6 @@ 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 @Test
fun `version one plaintext database migrates without losing history`() { fun `version one plaintext database migrates without losing history`() {
database.close() database.close()

View File

@ -29,16 +29,9 @@ Existing TLVs (unchanged):
Other optional extension: Other optional extension:
- `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Assigned bits - `0x05` CAPABILITIES: Minimal little-endian feature bitfield. Bit 8 advertises
match iOS: 0 prekeys, 1 Wi-Fi bulk, 2 gateway, 3 private groups, 4 board, authenticated Noise private media (`PRIVATE_MEDIA_V1`); its exact value is
5 vouch, 6 mesh diagnostics, 7 bridge, 8 authenticated Noise private media, `00 01`. An empty value is valid and means no advertised capabilities.
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. 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): New TLV (optional):
@ -76,12 +69,6 @@ This matches the onwire 8byte `senderID`/`recipientID` encoding used in th
security-sensitive bit is trusted only after the signed announcement key is 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 bound to the matching remote static key from a live Noise handshake; see
`PRIVATE_MEDIA_V1.md`. `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.
- 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: - If a `0x04` TLV is present:
- Interpret the value as `N = length / 8` peer IDs (ignore trailing nonaligned bytes). - Interpret the value as `N = length / 8` peer IDs (ignore trailing nonaligned bytes).
- Each 8byte chunk is decoded back to a 16hexchar peer ID string (lowercase). - Each 8byte chunk is decoded back to a 16hexchar peer ID string (lowercase).

View File

@ -31,15 +31,10 @@ GitHub/Google Play publication checklist.
The build uses a clean source tree, an isolated Gradle user home, UTC, a stable The build uses a clean source tree, an isolated Gradle user home, UTC, a stable
locale, `SOURCE_DATE_EPOCH` from the Git commit, no Gradle build or configuration locale, `SOURCE_DATE_EPOCH` from the Git commit, no Gradle build or configuration
cache, fresh tasks, a non-incremental in-process Kotlin compiler, and R8's cache, fresh tasks, and a non-incremental in-process Kotlin compiler. Native
deterministic-debugging mode. The R8 mode uses one compiler thread and disables builds remap source paths and release validation rejects host paths in packaged
randomized input shuffling. The regular R8 ProGuard map remains embedded in each libraries. The container overlays a canonical `local.properties`, so an ignored
AAB. Kotlin 2.4.10's optional Compose group-key mapping augmentation is disabled Android Studio file cannot redirect Gradle to a host-specific SDK.
because its duplicate-key selection depends on unspecified class-file iteration
order across clean builds. Native builds remap source paths and release
validation rejects host paths in packaged libraries. The container overlays a
canonical `local.properties`, so an ignored Android Studio file cannot redirect
Gradle to a host-specific SDK.
AGP's embedded VCS record is disabled because its Git discovery depends on the AGP's embedded VCS record is disabled because its Git discovery depends on the
host checkout layout. The canonical `BUILDINFO.json` and GitHub provenance host checkout layout. The canonical `BUILDINFO.json` and GitHub provenance
@ -59,10 +54,8 @@ The authoritative pins are:
## Reproduce a release locally ## Reproduce a release locally
Requirements are Git, Docker with Linux/amd64 support, more than 8 GiB of memory Requirements are Git, Docker with Linux/amd64 support, and enough free space for
available to the Docker VM (16 GiB recommended), and enough free space for the the Android and Gradle images and dependencies.
Android and Gradle images and dependencies. R8's single-threaded deterministic
mode can exceed an 8 GiB Docker memory limit while optimizing the phone app.
```bash ```bash
git clone https://github.com/permissionlesstech/bitchat-android.git git clone https://github.com/permissionlesstech/bitchat-android.git

View File

@ -2147,9 +2147,6 @@
<artifact name="aapt2-9.3.1-15703166-osx.jar"> <artifact name="aapt2-9.3.1-15703166-osx.jar">
<sha256 value="1e35bc2ce18c3aae840be2a29659ce50d6043e907a44d98ee1cf375d044fa29c" origin="Generated by Gradle"/> <sha256 value="1e35bc2ce18c3aae840be2a29659ce50d6043e907a44d98ee1cf375d044fa29c" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="aapt2-9.3.1-15703166-windows.jar">
<sha256 value="b1006ecec7e5936257e95e97f3eba7ef439d3e44178967cc048f86c9119fb231" origin="Generated by Gradle"/>
</artifact>
<artifact name="aapt2-9.3.1-15703166.pom"> <artifact name="aapt2-9.3.1-15703166.pom">
<sha256 value="edb82dcbe738ca78b31612cb307bf99e5f02f25c8125048f933e792fd7778beb" origin="Generated by Gradle"/> <sha256 value="edb82dcbe738ca78b31612cb307bf99e5f02f25c8125048f933e792fd7778beb" origin="Generated by Gradle"/>
</artifact> </artifact>
@ -4914,9 +4911,6 @@
</artifact> </artifact>
</component> </component>
<component group="org.junit" name="junit-bom" version="5.9.2"> <component group="org.junit" name="junit-bom" version="5.9.2">
<artifact name="junit-bom-5.9.2.module">
<sha256 value="ab137ba5a8e32c9b066bf9126a1c76dd5614b724ba5c0b02549772b5e9f4cf1f" origin="Generated by Gradle"/>
</artifact>
<artifact name="junit-bom-5.9.2.pom"> <artifact name="junit-bom-5.9.2.pom">
<sha256 value="2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705" origin="Generated by Gradle"/> <sha256 value="2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705" origin="Generated by Gradle"/>
</artifact> </artifact>

View File

@ -42,9 +42,6 @@ if ! [[ "$SOURCE_DATE_EPOCH" =~ ^[0-9]+$ ]]; then
fi fi
gradle_args=( gradle_args=(
# R8 otherwise randomizes input traversal and uses multiple compiler threads.
# The optimized DEX can still match while its embedded ProGuard map differs.
-Dcom.android.tools.r8.deterministicdebugging=1
-Pkotlin.compiler.execution.strategy=in-process -Pkotlin.compiler.execution.strategy=in-process
-Pkotlin.incremental=false -Pkotlin.incremental=false
--no-build-cache --no-build-cache

View File

@ -19,8 +19,8 @@ android {
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
// Wear releases use a separate high range because Play requires every artifact in // 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. // one application ID to have a unique version code across all form factors.
versionCode = 1_000_000_003 versionCode = 1_000_000_001
versionName = "0.1.2" versionName = "0.1.0"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
@ -65,13 +65,6 @@ android {
} }
} }
composeCompiler {
// Kotlin 2.4.10's optional Compose group-key mapping depends on unspecified
// class-file iteration order. Keep the normal R8 mapping, but omit that
// augmentation until its producer is deterministic across clean builds.
includeComposeMappingFile.set(false)
}
// Shared bitchat protocol stack: compiled from :app sources in place (never moved/copied by hand). // Shared bitchat protocol stack: compiled from :app sources in place (never moved/copied by hand).
// AGP's source directory sets no longer support include/exclude filters, so a Sync task // AGP's source directory sets no longer support include/exclude filters, so a Sync task
// materializes a filtered mirror into build/sharedSrc and that directory is added as a source // materializes a filtered mirror into build/sharedSrc and that directory is added as a source

View File

@ -8,6 +8,7 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -20,20 +21,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect 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.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
@ -50,7 +44,6 @@ import com.bitchat.watch.ui.theme.ChatVisualTokens
import com.bitchat.watch.ui.theme.LocalBitchatPalette import com.bitchat.watch.ui.theme.LocalBitchatPalette
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlin.math.sign
/** /**
* The shared chat body for global chat and DM threads, following the classic messenger * The shared chat body for global chat and DM threads, following the classic messenger
@ -89,34 +82,35 @@ fun ChatScaffold(
previousCount = messages.size previousCount = messages.size
} }
// Follow intent is changed only by an actual user scroll away from the newest item or by // One state drives both overlays: visible at the bottom or when scrolling toward it,
// reaching the end again. A new item temporarily makes canScrollForward true before layout; // hidden when scrolling up into history. The 24px (~12dp) threshold is deliberately
// treating that transient range change as user intent breaks automatic following. // small so the controls answer every flick immediately.
var followNewest by remember { mutableStateOf(true) } var atNewest by remember { mutableStateOf(true) }
val controlsVisible = remember { mutableStateOf(true) } val controlsVisible = remember { mutableStateOf(true) }
LaunchedEffect(columnState) { LaunchedEffect(columnState) {
var lastPosition = -1 var lastPosition = -1
var scrollIntent = ChatScrollIntentState()
snapshotFlow { snapshotFlow {
val first = columnState.layoutInfo.visibleItems.firstOrNull() val first = columnState.layoutInfo.visibleItems.firstOrNull()
ChatScrollSnapshot( Triple(columnState.canScrollForward, first?.index ?: 0, first?.offset ?: 0)
canScrollForward = columnState.canScrollForward, }.collect { (canScrollForward, index, offset) ->
isScrollInProgress = columnState.isScrollInProgress, val position = index * 100_000 + offset
position = (first?.index ?: 0) * 100_000 + (first?.offset ?: 0) atNewest = !canScrollForward
) if (!canScrollForward) {
}.collect { snapshot -> controlsVisible.value = true
scrollIntent = updatedChatScrollIntent( } else if (lastPosition >= 0) {
current = scrollIntent, when {
snapshot = snapshot, position > lastPosition + 24 -> controlsVisible.value = true
previousPosition = lastPosition position < lastPosition - 24 -> controlsVisible.value = false
) }
followNewest = scrollIntent.followsNewest }
controlsVisible.value = scrollIntent.controlsVisible lastPosition = position
lastPosition = snapshot.position
} }
} }
// Stick to bottom when the user has not intentionally moved into history. // 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 }
LaunchedEffect(columnState, messages.size) { LaunchedEffect(columnState, messages.size) {
if (messages.isNotEmpty() && followNewest) { if (messages.isNotEmpty() && followNewest) {
val expectedSingleMessageKey = messages.singleOrNull()?.id val expectedSingleMessageKey = messages.singleOrNull()?.id
@ -161,50 +155,6 @@ internal data class MeasuredChatLayout(
val singleVisibleItemKey: Any? 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
)
internal fun updatedChatScrollIntent(
current: ChatScrollIntentState,
snapshot: ChatScrollSnapshot,
previousPosition: Int
): 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( internal suspend fun scrollToNewestAfterItemsMeasured(
expectedItemCount: Int, expectedItemCount: Int,
expectedSingleMessageKey: Any?, expectedSingleMessageKey: Any?,
@ -235,7 +185,7 @@ private fun ChatBody(
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
val context = LocalContext.current val context = LocalContext.current
val transformationSpec = rememberTransformationSpec() val transformationSpec = rememberTransformationSpec()
val isScreenRound = LocalConfiguration.current.isScreenRound
// Slide-to-cancel: while recording, the finger's position is tracked globally; the // 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 // 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). // finger hovers it (with generous slack so the snap engages on approach).
@ -306,65 +256,16 @@ private fun ChatBody(
} }
} }
) { ) {
ScreenScaffold(scrollState = columnState) { scaffoldPadding -> ScreenScaffold(scrollState = columnState) {
val layoutDirection = LocalLayoutDirection.current
TransformingLazyColumn( TransformingLazyColumn(
state = columnState, state = columnState,
// Keep the list full-screen and geometrically unclipped. Wear's transformation modifier = Modifier.fillMaxSize(),
// 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
}
.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.
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
)
}
onDrawWithContent {
drawContent()
drawRect(
brush = edgeMask,
blendMode = BlendMode.DstIn
)
}
},
// Arrangement.Bottom anchors short content to the bottom: the first message // Arrangement.Bottom anchors short content to the bottom: the first message
// starts just above the action bar and new messages push history upward. // starts just above the action bar and new messages push history upward.
// The scroll range reserves resting space for the floating controls while the // The padding reserves permanent room for the floating header and action
// full-screen viewport preserves autoscroll and the native transformation focal // bar; being constant, it never disturbs an in-flight scroll gesture.
// point. Rows may travel behind the overlays only after they have begun the Wear
// edge scale/fade treatment.
verticalArrangement = Arrangement.Bottom, verticalArrangement = Arrangement.Bottom,
// Keep Wear Material's responsive horizontal inset while replacing its vertical contentPadding = PaddingValues(top = 30.dp, bottom = 64.dp)
// 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()) { if (messages.isEmpty()) {
item { item {
@ -398,12 +299,7 @@ private fun ChatBody(
// The header stays put and shrinks to its dense form instead of disappearing; // 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. // as an overlay its size animation never touches the list's scroll geometry.
Box( Box(modifier = Modifier.align(Alignment.TopCenter)) {
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth(),
contentAlignment = Alignment.TopCenter
) {
header(controlsVisible) header(controlsVisible)
} }
@ -437,12 +333,6 @@ private fun ChatBody(
// Extra finger slack (px, ~28dp at watch density) around the cancel target so the snap // 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. // engages as the finger approaches, not only on exact contact.
private const val CANCEL_HOVER_SLANT_PX = 56f 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 // 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 // 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. // boundary); it leans toward the finger by up to MAGNET_PULL_PX.

View File

@ -67,11 +67,10 @@ fun PeopleScreen(onOpenDm: (String) -> Unit, onEditNickname: () -> Unit) {
) )
} }
ScreenScaffold(scrollState = listState) { scaffoldPadding -> ScreenScaffold(scrollState = listState) {
ScalingLazyColumn( ScalingLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize()
contentPadding = scaffoldPadding
) { ) {
item { item {
ListHeader { ListHeader {

View File

@ -1,6 +1,7 @@
package com.bitchat.watch.ui package com.bitchat.watch.ui
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -14,7 +15,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -48,17 +48,11 @@ fun UserDetailScreen(
val listState = rememberScalingLazyListState() val listState = rememberScalingLazyListState()
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
ScreenScaffold(scrollState = listState) { scaffoldPadding -> ScreenScaffold(scrollState = listState) {
val layoutDirection = LocalLayoutDirection.current
ScalingLazyColumn( ScalingLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = scaffoldPadding contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)
.withAdditionalPadding(
layoutDirection = layoutDirection,
horizontal = 10.dp,
vertical = 8.dp
)
) { ) {
item { item {
ListHeader { ListHeader {

View File

@ -2,6 +2,7 @@ package com.bitchat.watch.ui
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -13,7 +14,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -43,17 +43,11 @@ fun VerificationCodeScreen(peerID: String) {
val listState = rememberScalingLazyListState() val listState = rememberScalingLazyListState()
val palette = LocalBitchatPalette.current val palette = LocalBitchatPalette.current
ScreenScaffold(scrollState = listState) { scaffoldPadding -> ScreenScaffold(scrollState = listState) {
val layoutDirection = LocalLayoutDirection.current
ScalingLazyColumn( ScalingLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = scaffoldPadding contentPadding = PaddingValues(horizontal = 10.dp, vertical = 8.dp)
.withAdditionalPadding(
layoutDirection = layoutDirection,
horizontal = 10.dp,
vertical = 8.dp
)
) { ) {
item { item {
ListHeader { ListHeader {

View File

@ -1,53 +0,0 @@
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 {
return PaddingValues(
start = calculateStartPadding(layoutDirection) + horizontal,
top = calculateTopPadding() + vertical,
end = calculateEndPadding(layoutDirection) + horizontal,
bottom = calculateBottomPadding() + vertical
)
}
/**
* 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.withVerticalClearance(
layoutDirection: LayoutDirection,
top: Dp,
bottom: Dp
): PaddingValues {
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)
}
private fun PaddingValues.calculateEndPadding(layoutDirection: LayoutDirection): Dp =
when (layoutDirection) {
LayoutDirection.Ltr -> calculateRightPadding(layoutDirection)
LayoutDirection.Rtl -> calculateLeftPadding(layoutDirection)
}

View File

@ -10,102 +10,6 @@ import org.junit.Test
class ChatAutoScrollTest { 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)
}
@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)
val dockedAgain = updatedChatScrollIntent(
current = browsingHistory,
snapshot = ChatScrollSnapshot(
canScrollForward = false,
isScrollInProgress = false,
position = 200
),
previousPosition = 60
)
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 @Test
fun `newest scroll waits until appended message is measured`() = runTest { fun `newest scroll waits until appended message is measured`() = runTest {
val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null)) val measuredLayouts = MutableStateFlow(MeasuredChatLayout(3, null))

View File

@ -1,70 +0,0 @@
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 `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)
.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(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())
}
}