diff --git a/app/src/main/assets/nostr_relays.csv b/app/src/main/assets/nostr_relays.csv index eb5e8c277..ab075fb4d 100644 --- a/app/src/main/assets/nostr_relays.csv +++ b/app/src/main/assets/nostr_relays.csv @@ -1,269 +1,295 @@ Relay URL,Latitude,Longitude -relay-admin.thaliyal.com,40.8218,-74.45 -nostr.notribe.net,40.8302,-74.1299 -strfry.bonsai.com,37.8715,-122.273 -nostr-relay.online,40.7357,-74.1724 -shu05.shugur.net,48.8566,2.35222 -dev-nostr.bityacht.io,25.0797,121.234 -relay.nostrhub.tech,49.0291,8.35696 -relay.davidebtc.me,50.1109,8.68213 -relay.moinsen.com,50.4754,12.3683 -relay.olas.app,50.4754,12.3683 -mhp258zrpiiwn.clorecloud.net,43.6532,-79.3832 -orangepiller.org,60.1699,24.9384 -relayrs.notoshi.win,43.6532,-79.3832 -relay.guggero.org,47.3769,8.54169 +strfry.shock.network,39.0438,-77.4874 +wot.nostr.party,36.1627,-86.7816 nostr.blankfors.se,60.1699,24.9384 -wot.sovbit.host,64.1466,-21.9426 -nostr.huszonegy.world,47.4979,19.0402 -wot.sebastix.social,51.8933,4.42083 -articles.layer3.news,37.3387,-121.885 -nostr.spicyz.io,40.7357,-74.1724 -nostr.jerrynya.fun,31.2304,121.474 -nostr.oxtr.dev,50.4754,12.3683 -relay.mwaters.net,50.9871,2.12554 -vitor.nostr1.com,40.7128,-74.006 -relay.lumina.rocks,49.0291,8.35695 -nostr-relay-1.trustlessenterprise.com,40.7357,-74.1724 -nostr.bilthon.dev,25.8128,-80.2377 nostr.now,36.55,139.733 -nostr.girino.org,40.7357,-74.1724 -nostr-01.yakihonne.com,1.32123,103.695 -ribo.af.nostria.app,-26.2041,28.0473 +nostr.simplex.icu,51.5121,-0.0005238 +relay.threenine.services,51.5524,-0.29686 +relay.barine.co,43.6532,-79.3832 +theoutpost.life,64.1476,-21.9392 +purplerelay.com,50.1109,8.68213 +nostrue.com,40.8054,-74.0241 +relay-arg.zombi.cloudrodion.com,1.35208,103.82 +shu02.shugur.net,21.4902,39.2246 +relayone.geektank.ai,18.2148,-63.0574 +relay03.lnfi.network,39.0997,-94.5786 +soloco.nl,43.6532,-79.3832 +relay-testnet.k8s.layer3.news,37.3387,-121.885 +nostr.azzamo.net,52.2633,21.0283 +nostr.red5d.dev,43.6532,-79.3832 +nostr.thebiglake.org,32.71,-96.6745 +nostr.tadryanom.me,43.6532,-79.3832 +schnorr.me,43.6532,-79.3832 +ithurtswhenip.ee,51.223,6.78245 +orly.ft.hn,50.4754,12.3683 +relay02.lnfi.network,39.0997,-94.5786 +relay.nosto.re,51.1792,5.89444 +strfry.openhoofd.nl,51.9229,4.40833 +relays.diggoo.com,43.6532,-79.3832 +relay.nostrhub.tech,49.0291,8.35696 +relay.artx.market,43.652,-79.3633 +relay.sigit.io,50.4754,12.3683 +nostr-relay.psfoundation.info,39.0438,-77.4874 +wot.brightbolt.net,47.6735,-116.781 +relay.bitcoinartclock.com,50.4754,12.3683 +khatru.nostrver.se,51.1792,5.89444 +relay.cosmicbolt.net,37.3986,-121.964 +adre.su,59.9311,30.3609 nostrelay.memory-art.xyz,43.6532,-79.3832 -relay.evanverma.com,40.8302,-74.1299 -a.nos.lol,50.4754,12.3683 -purpura.cloud,40.7357,-74.1724 -relay.snort.social,43.6532,-79.3832 -relay.holzeis.me,43.6532,-79.3832 -nostr.spaceshell.xyz,40.7128,-74.006 -nostr.liberty.fans,36.9104,-89.5875 -relay.fundstr.me,42.3601,-71.0589 -wot.basspistol.org,49.4521,11.0767 -ribo.eu.nostria.app,52.3676,4.90414 -relay.notoshi.win,13.4166,101.335 -relay.stream.labs.h3.se,59.4016,17.9455 +temp.iris.to,43.6532,-79.3832 +nproxy.kristapsk.lv,60.1699,24.9384 +relay.ngengine.org,43.6532,-79.3832 +relay.puresignal.news,43.6532,-79.3832 nostr.stakey.net,52.3676,4.90414 -relay.satlantis.io,32.8769,-80.0114 -nostr-2.21crypto.ch,47.4988,8.72369 -nostr.satstralia.com,64.1476,-21.9392 -slick.mjex.me,39.048,-77.4817 -nostr-relay.nextblockvending.com,47.2343,-119.853 -relay.origin.land,35.6673,139.751 -nostr.fbxl.net,48.382,-89.2502 +nostr.commonshub.brussels,49.4543,11.0746 +wot.shaving.kiwi,43.6532,-79.3832 +espelho.girino.org,43.6532,-79.3832 +relay.thibautduchene.fr,43.6532,-79.3832 +nostr-relay.corb.net,38.8353,-104.822 +wot.soundhsa.com,33.1384,-95.6011 +relay.jabato.space,52.52,13.405 +relay.credenso.cafe,43.3601,-80.3127 +relayone.soundhsa.com,33.1384,-95.6011 +relay.bitcoinveneto.org,64.1466,-21.9426 +relay.samt.st,40.8302,-74.1299 +relay.vrtmrz.net,43.6532,-79.3832 +nostr.data.haus,50.4754,12.3683 +relay.npubhaus.com,43.6532,-79.3832 +strfry.elswa-dev.online,50.1109,8.68213 +relay.wavefunc.live,34.0362,-118.443 +wot.sovbit.host,64.1466,-21.9426 +relay.smies.me,33.7501,-84.3885 +nostr.bilthon.dev,25.8128,-80.2377 +relay-fra.zombi.cloudrodion.com,48.8566,2.35222 +santo.iguanatech.net,40.8302,-74.1299 +prl.plus,42.6978,23.3246 +relay-freeharmonypeople.space,38.7223,-9.13934 +nostream.breadslice.com,1.35208,103.82 +nostr.notribe.net,40.8302,-74.1299 +relay.fountain.fm,39.0997,-94.5786 +orangepiller.org,60.1699,24.9384 +nostr.21crypto.ch,47.5356,8.73209 +nostr.camalolo.com,24.1469,120.684 +okn.czas.top,51.267,6.81738 +relay.wolfcoil.com,35.6092,139.73 +nostr.quali.chat,60.1699,24.9384 +nostr-relay.online,43.6532,-79.3832 +alienos.libretechsystems.xyz,55.4724,9.87335 +ribo.eu.nostria.app,52.3676,4.90414 +wot.yesnostr.net,50.9871,2.12554 +nostr.overmind.lol,43.6532,-79.3832 +relay.chakany.systems,43.6532,-79.3832 +relay.hasenpfeffr.com,39.0438,-77.4874 +nostrelites.org,41.8781,-87.6298 +strfry.bonsai.com,37.8715,-122.273 +relay.agora.social,50.7383,15.0648 +alien.macneilmediagroup.com,43.6532,-79.3832 +relay.getsafebox.app,43.6532,-79.3832 +relay.nuts.cash,34.0362,-118.443 +notemine.io,52.2026,20.9397 relay.nostr.place,32.7767,-96.797 -nr.yay.so,46.2126,6.1154 -nostream.breadslice.com,40.7357,-74.1724 -wot.tealeaf.dev,33.7488,-84.3877 -relay.primal.net,43.6532,-79.3832 +relay.21e6.cz,50.7383,15.0648 +nostr.casa21.space,43.6532,-79.3832 +premium.primal.net,43.6532,-79.3832 +relay5.bitransfer.org,43.6532,-79.3832 +nostr.rblb.it,43.7094,10.6582 +relay.goodmorningbitcoin.com,43.6532,-79.3832 +relay.camelus.app,45.5201,-122.99 +relay.origin.land,35.6673,139.751 +relay.fr13nd5.com,52.5233,13.3426 +nos.lol,50.4754,12.3683 +wot.nostr.net,43.6532,-79.3832 +relay.javi.space,43.4633,11.8796 +relay2.ngengine.org,43.6532,-79.3832 +relay.angor.io,48.1046,11.6002 +pyramid.treegaze.com,43.6532,-79.3832 +relay.letsfo.com,52.2633,21.0283 +relay.nostrzh.org,43.6532,-79.3832 +nostr.hekster.org,37.3986,-121.964 +relay.0xchat.com,1.35208,103.82 +nostr.czas.top,50.1109,8.68213 +nostr.superfriends.online,43.6532,-79.3832 +hsuite-nostr-relay.hbarsuite.workers.dev,43.6532,-79.3832 +relay.mccormick.cx,52.3563,4.95714 +nostr.88mph.life,51.5072,-0.127586 +strfry.ymir.cloud,34.0965,-117.585 +nostr.lkjsxc.com,43.6532,-79.3832 +yabu.me,35.6092,139.73 relay.chorus.community,50.1109,8.68213 -wot.dergigi.com,64.1476,-21.9392 +nostr-02.yakihonne.com,1.32123,103.695 +neuromancer.nettek.io,39.1429,-94.573 +relay04.lnfi.network,39.0997,-94.5786 nostr-relay.amethyst.name,39.0438,-77.4874 -nostr.mehdibekhtaoui.com,49.4939,-1.54813 -relay.mess.ch,46.948,7.44745 -relay.sigit.io,50.4754,12.3683 -relay-rpi.edufeed.org,49.4543,11.0746 +nostrcheck.tnsor.network,43.6532,-79.3832 +wot.nostr.place,32.7767,-96.797 +cyberspace.nostr1.com,40.7057,-74.0136 +relay.guggero.org,47.3769,8.54169 +nostr.calitabby.net,39.9268,-75.0246 +x.kojira.io,43.6532,-79.3832 +nostr.agentcampfire.com,52.3676,4.90414 +relay.bullishbounty.com,43.6532,-79.3832 +relay.nostrverse.net,43.6532,-79.3832 +nostr.robosats.org,64.1476,-21.9392 +nostr-verified.wellorder.net,45.5201,-122.99 +nostr.huszonegy.world,47.4979,19.0402 +relay.cypherflow.ai,48.8566,2.35222 +nostr-relay.xbytez.io,50.6924,3.20113 nostr.faultables.net,43.6532,-79.3832 -relay.getsafebox.app,43.6532,-79.3832 -cyberspace.nostr1.com,40.7128,-74.006 -relay.endfiat.money,43.6532,-79.3832 -soloco.nl,43.6532,-79.3832 -nostr.kungfu-g.rip,33.7946,-84.4488 -nostrelay.circum.space,51.2217,6.77616 -relay.trustroots.org,43.6532,-79.3832 +relay.libernet.app,43.6532,-79.3832 +relay.magiccity.live,25.8128,-80.2377 relay.wellorder.net,45.5201,-122.99 -relay.coinos.io,40.7357,-74.1724 -relay-testnet.k8s.layer3.news,37.3387,-121.885 -relay.nostriot.com,41.5695,-83.9786 -relay.bitcoinartclock.com,50.4754,12.3683 -nostr.einundzwanzig.space,50.1109,8.68213 -nostr.casa21.space,43.6532,-79.3832 -premium.primal.net,40.7357,-74.1724 -relay.tagayasu.xyz,43.6715,-79.38 -nostr.mom,50.4754,12.3683 -nostr.zenon.network,43.5009,-70.4428 -nostr-pub.wellorder.net,45.5201,-122.99 -relay.g1sms.fr,43.9432,2.07537 -nostr-rs-relay-ishosta.phamthanh.me,40.7357,-74.1724 -relay.illuminodes.com,47.6061,-122.333 -dizzyspells.nostr1.com,40.7057,-74.0136 -relay.mostro.network,40.8302,-74.1299 +black.nostrcity.club,48.8575,2.35138 +relay.jeffg.fyi,43.6532,-79.3832 +freeben666.fr,43.7221,7.15296 relay.nostr.wirednet.jp,34.706,135.493 -relay.barine.co,43.6532,-79.3832 -relay.damus.io,43.6532,-79.3832 -relay.0xchat.com,1.35208,103.82 -relay.mattybs.lol,40.7357,-74.1724 -no.str.cr,9.92857,-84.0528 -relay.utxo.farm,35.6916,139.768 -nostr.pleb.one,38.6327,-90.1961 -relay-dev.satlantis.io,40.8302,-74.1299 -relay.nostrdice.com,-33.8688,151.209 -relay.nostraddress.com,40.7357,-74.1724 -satsage.xyz,37.3986,-121.964 -offchain.pub,36.1809,-115.241 -noxir.kpherox.dev,34.8587,135.509 -nostr-relay.psfoundation.info,39.0438,-77.4874 -khatru.nostrver.se,51.8933,4.42083 -purplerelay.com,50.1109,8.68213 -relay.tapestry.ninja,40.8054,-74.0241 -nostr.night7.space,50.4754,12.3683 -nostr.rikmeijer.nl,50.4754,12.3683 -relay1.nostrchat.io,60.1699,24.9384 -nostr.21crypto.ch,47.4988,8.72369 -wot.soundhsa.com,33.1384,-95.6011 -relay.orangepill.ovh,49.1689,-0.358841 -talon.quest,43.6532,-79.3832 +nostrelay.circum.space,52.3676,4.90414 +relay.islandbitcoin.com,12.8498,77.6545 nostr-rs-relay.dev.fedibtc.com,39.0438,-77.4874 -wot.codingarena.top,50.4754,12.3683 -fanfares.nostr1.com,40.7128,-74.006 -gnostr.com,42.6978,23.3246 -nostrcheck.tnsor.network,40.7357,-74.1724 -nostrelites.org,41.8781,-87.6298 +relay.nostriot.com,41.5695,-83.9786 +relayrs.notoshi.win,43.6532,-79.3832 +nostr-pub.wellorder.net,45.5201,-122.99 +nostr.vulpem.com,49.4543,11.0746 +nostr.sathoarder.com,48.5734,7.75211 +wheat.happytavern.co,43.6532,-79.3832 +relay.nostr.net,43.6532,-79.3832 +bcast.girino.org,43.6532,-79.3832 +nostr-02.czas.top,51.2277,6.77346 +vault.iris.to,43.6532,-79.3832 +ynostr.yael.at,60.1699,24.9384 +nostr.nodesmap.com,59.3327,18.0656 +nostr.n7ekb.net,47.4941,-122.294 +relayb.uid.ovh,43.6532,-79.3832 +shu05.shugur.net,48.8566,2.35222 +dev-relay.lnfi.network,39.0997,-94.5786 relay.bitcoindistrict.org,43.6532,-79.3832 -relay.fr13nd5.com,52.5233,13.3426 -wot.nostr.place,30.2672,-97.7431 -ithurtswhenip.ee,51.223,6.78245 +nostr.spicyz.io,43.6532,-79.3832 +nostr.0x7e.xyz,47.4988,8.72369 relay.dwadziesciajeden.pl,52.2297,21.0122 -relay2.ngengine.org,40.7357,-74.1724 -relay.nostr.net,50.4754,12.3683 -nostr-relay.cbrx.io,40.7357,-74.1724 -dev-relay.lnfi.network,39.0997,-94.5786 -relay.jeffg.fyi,43.6532,-79.3832 -relay.ngengine.org,40.7357,-74.1724 +relay.zone667.com,60.1699,24.9384 +nostr-dev.wellorder.net,45.5201,-122.99 nos.xmark.cc,50.6924,3.20113 -relay.21e6.cz,50.1682,14.0546 +relay.etch.social,41.2619,-95.8608 +nostr.na.social,43.6532,-79.3832 +relay.orangepill.ovh,49.1689,-0.358841 +relay.olas.app,50.4754,12.3683 +relay.holzeis.me,43.6532,-79.3832 +relay2.angor.io,48.1046,11.6002 relay.degmods.com,50.4754,12.3683 -nostr.coincrowd.fund,39.0438,-77.4874 +vitor.nostr1.com,40.7128,-74.006 +relay.btcforplebs.com,43.6532,-79.3832 +nostr.luisschwab.net,43.6532,-79.3832 +relay.moinsen.com,50.4754,12.3683 +czas.xyz,48.8566,2.35222 +nostr.bitcoiner.social,39.1585,-94.5728 +nostr.mehdibekhtaoui.com,49.4939,-1.54813 +inbox.azzamo.net,52.2633,21.0283 +nostr.ovia.to,43.6532,-79.3832 nostr.myshosholoza.co.za,52.3676,4.90414 -relay.digitalezukunft.cyou,45.5019,-73.5674 -r.lostr.net,52.3676,4.90414 -relay.etch.social,41.2619,-95.8608 -nostr.tac.lol,47.4748,-122.273 -nostr.azzamo.net,52.2633,21.0283 -nostr.4rs.nl,49.0291,8.35696 -nostr-03.dorafactory.org,1.35208,103.82 -relay.copylaradio.com,51.223,6.78245 -nostr.camalolo.com,24.1469,120.684 -nostr-dev.wellorder.net,45.5201,-122.99 -relay.nostx.io,43.6532,-79.3832 +fenrir-s.notoshi.win,43.6532,-79.3832 +relay-rpi.edufeed.org,49.4521,11.0767 +chat-relay.zap-work.com,43.6532,-79.3832 +nostr.bond,50.1109,8.68213 +relay01.lnfi.network,39.0997,-94.5786 +relay.seq1.net,43.6532,-79.3832 +nostr.rikmeijer.nl,50.4754,12.3683 +offchain.pub,47.6743,-117.112 +nostr.spaceshell.xyz,43.6532,-79.3832 +nos4smartnkind.tech,40.1872,44.5152 +kotukonostr.onrender.com,37.7775,-122.397 +nostr.ps1829.com,33.8851,130.883 +relay.lightning.pub,39.0438,-77.4874 +nostr-relay.gateway.in.th,15.2634,100.344 +relay.thebluepulse.com,49.4521,11.0767 +relay.malxte.de,52.52,13.405 +relay.usefusion.ai,38.7134,-78.1591 +nostr-relay.cbrx.io,43.6532,-79.3832 +shu01.shugur.net,21.4902,39.2246 +nostr.jerrynya.fun,31.2304,121.474 +dev-nostr.bityacht.io,25.0797,121.234 +bitsat.molonlabe.holdings,51.4012,-1.3147 +relay.primal.net,43.6532,-79.3832 +relay.wavlake.com,41.2619,-95.8608 +nostr-relay.nextblockvending.com,47.2343,-119.853 +v-relay.d02.vrtmrz.net,34.6937,135.502 +nostr.mikoshi.de,47.74,12.0917 +nostr.coincards.com,53.5501,-113.469 +relay.snort.social,53.3498,-6.26031 +bitcoiner.social,39.1585,-94.5728 +nostr.noones.com,50.1109,8.68213 +wot.dergigi.com,64.1476,-21.9392 +relay.lumina.rocks,49.0291,8.35695 +nostr-rs-relay-ishosta.phamthanh.me,43.6532,-79.3832 +nostr.snowbla.de,60.1699,24.9384 +strfry.felixzieger.de,50.1013,8.62643 +relay.satlantis.io,32.8769,-80.0114 +relay-dev.satlantis.io,40.8302,-74.1299 +nostr.davidebtc.me,51.5072,-0.127586 +relay.mattybs.lol,43.6532,-79.3832 +relay.fundstr.me,42.3601,-71.0589 +relay.tagayasu.xyz,43.6715,-79.38 +nostr-relay.zimage.com,34.0549,-118.243 +nostrcheck.me,43.6532,-79.3832 +bucket.coracle.social,37.7775,-122.397 +relay.siamdev.cc,13.9178,100.424 r.bitcoinhold.net,43.6532,-79.3832 -nproxy.kristapsk.lv,60.1699,24.9384 -adre.su,59.9311,30.3609 -relay.hasenpfeffr.com,39.0438,-77.4874 -nos.lol,50.4754,12.3683 -relay.nostr.band,60.1699,24.9384 -nostr-02.czas.top,53.471,9.88208 -relay.nosto.re,51.8933,4.42083 +skeme.vanderwarker.family,40.8218,-74.45 +articles.layer3.news,37.3387,-121.885 +no.str.cr,9.92857,-84.0528 +srtrelay.c-stellar.net,43.6532,-79.3832 +relay.comcomponent.com,34.7062,135.493 +relay.illuminodes.com,47.6061,-122.333 +nostr.openhoofd.nl,51.9229,4.40833 nostr.plantroon.com,50.1013,8.62643 -nostr.rblb.it,43.4633,11.8796 -nostr.thebiglake.org,32.71,-96.6745 -nostr.luisschwab.net,40.7357,-74.1724 -relay.electriclifestyle.com,26.2897,-80.1293 -librerelay.aaroniumii.com,43.6532,-79.3832 -nostr.88mph.life,40.7357,-74.1724 -shu02.shugur.net,21.4902,39.2246 -relay.hook.cafe,40.7357,-74.1724 -strfry.elswa-dev.online,48.8566,2.35222 -wot.sudocarlos.com,51.5072,-0.127586 -relay.islandbitcoin.com,12.8498,77.6545 -nostr.tadryanom.me,40.7357,-74.1724 -relay.zone667.com,60.1699,24.9384 -nostr.agentcampfire.com,50.8933,6.05805 -relay.ditto.pub,40.7357,-74.1724 -relay03.lnfi.network,39.0997,-94.5786 -relay2.angor.io,48.1046,11.6002 -srtrelay.c-stellar.net,40.7357,-74.1724 -relayone.soundhsa.com,33.1384,-95.6011 -relay.javi.space,43.4633,11.8796 -nostr.carroarmato0.be,50.9928,3.26317 -nostr.hekster.org,37.3986,-121.964 -strfry.shock.network,41.8959,-88.2169 -nostr.2b9t.xyz,34.0549,-118.243 +slick.mjex.me,39.048,-77.4817 +relay.minibolt.info,43.6532,-79.3832 +nostr-relay-1.trustlessenterprise.com,43.6532,-79.3832 +relay.nostrdice.com,-33.8688,151.209 +wot.dtonon.com,43.6532,-79.3832 +shu04.shugur.net,25.2604,55.2989 +nostr.tavux.tech,48.8575,2.35138 +nostr-02.uid.ovh,43.6532,-79.3832 +relay.nostrcheck.me,43.6532,-79.3832 +relay.nsnip.io,60.1699,24.9384 relay.toastr.net,40.8054,-74.0241 -relay.bitcoinveneto.org,64.1466,-21.9426 -relay.wavlake.com,41.2619,-95.8608 +relay.divine.video,43.6532,-79.3832 +nostr.4rs.nl,49.0291,8.35696 +wot.sebastix.social,51.1792,5.89444 +relay.openfarmtools.org,60.1699,24.9384 +relay.damus.io,43.6532,-79.3832 +purpura.cloud,43.6532,-79.3832 +wot.sudocarlos.com,51.5072,-0.127586 relay.arx-ccn.com,50.4754,12.3683 -relay.cosmicbolt.net,37.3986,-121.964 -relay.mccormick.cx,52.3563,4.95714 -temp.iris.to,40.7357,-74.1724 -relay.vrtmrz.net,40.7357,-74.1724 -nostr-relay.zimage.com,34.282,-118.439 -nostr.data.haus,50.4754,12.3683 +nostr.zoracle.org,45.6018,-121.185 +nostr.tac.lol,47.4748,-122.273 +relay.coinos.io,43.6532,-79.3832 +nostr.rtvslawenia.com,49.4543,11.0746 +relay.davidebtc.me,51.5072,-0.127586 ribo.us.nostria.app,41.5868,-93.625 -nostr.vulpem.com,49.4543,11.0746 -relay.agora.social,50.7383,15.0648 -nostr.ovia.to,43.6532,-79.3832 -nostr.red5d.dev,40.7357,-74.1724 -orangesync.tech,50.1109,8.68213 -relay.fountain.fm,39.0997,-94.5786 -relay.aloftus.io,34.0881,-118.379 +relay.upleb.uk,51.9194,19.1451 +librerelay.aaroniumii.com,43.6532,-79.3832 +relay.endfiat.money,43.6532,-79.3832 +relay.nostar.org,43.6532,-79.3832 +relay.electriclifestyle.com,26.2897,-80.1293 +relay.nostrhub.fr,48.1045,11.6004 +relay.agorist.space,52.3734,4.89406 +freelay.sovbit.host,64.1476,-21.9392 nostr.hifish.org,47.4043,8.57398 -relay.siamdev.cc,13.9178,100.424 -fenrir-s.notoshi.win,43.6532,-79.3832 -nostr.overmind.lol,43.6532,-79.3832 -wheat.happytavern.co,40.7357,-74.1724 -nostr.rtvslawenia.com,49.4543,11.0746 -relay.nostrhub.fr,48.1046,11.6002 -strfry.openhoofd.nl,51.9229,4.40833 -relay.usefusion.ai,38.7134,-78.1591 -relay.credenso.cafe,43.3601,-80.3127 -nostr.lostr.space,40.7357,-74.1724 -relay.jmoose.rocks,60.1699,24.9384 -relay.nostromo.social,49.4543,11.0746 -nostr.jfischer.org,49.0291,8.35696 -relay.wolfcoil.com,35.6092,139.73 -nostr.thaliyal.com,40.8218,-74.45 -relay.magiccity.live,25.8128,-80.2377 -relay.puresignal.news,40.7357,-74.1724 -prl.plus,55.7623,37.6381 -wot.brightbolt.net,47.6735,-116.781 -relay.varke.eu,52.6921,6.19372 -alienos.libretechsystems.xyz,55.4724,9.87335 -relay.goodmorningbitcoin.com,43.6532,-79.3832 -pyramid.fiatjaf.com,51.5072,-0.127586 -relay02.lnfi.network,39.0997,-94.5786 -nostr.davidebtc.me,50.1109,8.68213 -nostr-verified.wellorder.net,45.5201,-122.99 -relay.cypherflow.ai,48.8566,2.35222 -nostr.snowbla.de,60.1699,24.9384 -inbox.azzamo.net,52.2633,21.0283 -shu01.shugur.net,21.4902,39.2246 -nostr.middling.mydns.jp,35.8099,140.12 -nostr.kalf.org,52.3676,4.90414 -relay.laantungir.net,-19.4692,-42.5315 -relay.angor.io,48.1046,11.6002 -nostr2.girino.org,40.7357,-74.1724 -relay01.lnfi.network,39.0997,-94.5786 +nostr-01.yakihonne.com,1.32123,103.695 +relay.routstr.com,43.6532,-79.3832 +nr.yay.so,46.2126,6.1154 +bcast.seutoba.com.br,43.6532,-79.3832 +fanfares.nostr1.com,40.7128,-74.006 +nostr.girino.org,43.6532,-79.3832 +nostr2.girino.org,43.6532,-79.3832 +relay.notoshi.win,13.311,101.112 +nostrja-kari.heguro.com,43.6532,-79.3832 +relay.mostro.network,40.8302,-74.1299 +satsage.xyz,37.3986,-121.964 +relay.evanverma.com,40.8302,-74.1299 +nostr-2.21crypto.ch,47.5356,8.73209 +relay.mitchelltribe.com,39.0438,-77.4874 +relaynostr.breadslice.com,43.6532,-79.3832 nostr.chaima.info,51.223,6.78245 -x.kojira.io,43.6532,-79.3832 -shu04.shugur.net,25.2604,55.2989 -santo.iguanatech.net,40.8302,-74.1299 -relay.artx.market,43.652,-79.3633 -alien.macneilmediagroup.com,40.7357,-74.1724 -nostr.sathoarder.com,48.5734,7.75211 -zap.watch,45.5029,-73.5723 -relay.basspistol.org,46.2044,6.14316 -relay.13room.space,43.6532,-79.3832 -relay.bullishbounty.com,40.7357,-74.1724 -theoutpost.life,64.1476,-21.9392 -nostr.coincards.com,53.5501,-113.469 -black.nostrcity.club,41.8781,-87.6298 -relay.npubhaus.com,40.7357,-74.1724 -relay.freeplace.nl,52.3676,4.90414 -relay.seq1.net,43.6532,-79.3832 -ynostr.yael.at,60.1699,24.9384 -relay.nostr.vet,52.6467,4.7395 -relay.lifpay.me,1.35208,103.82 -relay.chakany.systems,43.6532,-79.3832 -relay.lightning.pub,41.8959,-88.2169 -wot.dtonon.com,43.6532,-79.3832 -yabu.me,35.6092,139.73 -wot.nostr.net,43.6532,-79.3832 -relay.libernet.app,40.7357,-74.1724 -relay04.lnfi.network,39.0997,-94.5786 -nostr.0x7e.xyz,47.4988,8.72369 -nostr.mikoshi.de,50.1109,8.68213 -wot.nostr.party,36.1627,-86.7816 -relay.letsfo.com,51.098,17.0321 -nostr.makibisskey.work,43.6532,-79.3832 -nostr.simplex.icu,50.8198,-1.08798 +nostr.mom,50.4754,12.3683 +relay.nostr-check.me,43.6532,-79.3832 +relay.ditto.pub,43.6532,-79.3832 diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 4224d9c1f..ded8335de 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -429,7 +429,7 @@ class ChatViewModel( // Default: route via mesh meshService.sendMessage(messageContent, mentions, channel) } - }) + }, this) return } diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index 499b39261..8678b9822 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -2,10 +2,12 @@ package com.bitchat.android.ui import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.ui.ChatViewModel import java.util.Date /** - * Handles processing of IRC-style commands + * IRC-style command processor for BitChat. + * Handles both mesh network and geohash (Nostr) commands with proper context awareness. */ class CommandProcessor( private val state: ChatState, @@ -14,7 +16,12 @@ class CommandProcessor( private val privateChatManager: PrivateChatManager ) { - // Available commands list + companion object { + private const val NOSTR_PEER_PREFIX = "nostr_" + private const val NOSTR_ID_LENGTH = 32 + } + + // Base command definitions - available in all contexts private val baseCommands = listOf( CommandSuggestion("/block", emptyList(), "[nickname]", "block or list blocked peers"), CommandSuggestion("/channels", emptyList(), null, "show all discovered channels"), @@ -27,20 +34,26 @@ class CommandProcessor( CommandSuggestion("/w", emptyList(), null, "see who's online") ) - // MARK: - Command Processing + // Command processing entry point - fun processCommand(command: String, meshService: BluetoothMeshService, myPeerID: String, onSendMessage: (String, List, String?) -> Unit, viewModel: ChatViewModel? = null): Boolean { + fun processCommand( + command: String, + meshService: BluetoothMeshService, + myPeerID: String, + onSendMessage: (String, List, String?) -> Unit, + viewModel: ChatViewModel? = null + ): Boolean { if (!command.startsWith("/")) return false val parts = command.split(" ") val cmd = parts.first().lowercase() when (cmd) { - "/j", "/join" -> handleJoinCommand(parts, myPeerID) - "/m", "/msg" -> handleMessageCommand(parts, meshService) + "/j", "/join" -> handleJoinCommand(parts, myPeerID, viewModel) + "/m", "/msg" -> handleMessageCommand(parts, meshService, viewModel) "/w" -> handleWhoCommand(meshService, viewModel) "/clear" -> handleClearCommand() "/pass" -> handlePassCommand(parts, myPeerID) - "/block" -> handleBlockCommand(parts, meshService) + "/block" -> handleBlockCommand(parts, meshService, viewModel) "/unblock" -> handleUnblockCommand(parts, meshService) "/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage) "/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage) @@ -51,152 +64,165 @@ class CommandProcessor( return true } - private fun handleJoinCommand(parts: List, myPeerID: String) { - if (parts.size > 1) { - val channelName = parts[1] - val channel = if (channelName.startsWith("#")) channelName else "#$channelName" - val password = if (parts.size > 2) parts[2] else null - val success = channelManager.joinChannel(channel, password, myPeerID) - if (success) { - val systemMessage = BitchatMessage( - sender = "system", - content = "joined channel $channel", - timestamp = Date(), - isRelay = false - ) - messageManager.addMessage(systemMessage) - } - } else { - val systemMessage = BitchatMessage( + private fun handleJoinCommand(parts: List, myPeerID: String, viewModel: ChatViewModel? = null) { + // Geohash channels use Nostr, mesh channels use Bluetooth - they're separate systems + val currentLocation = state.selectedLocationChannel.value + if (currentLocation is com.bitchat.android.geohash.ChannelID.Location) { + messageManager.addMessage(BitchatMessage( + sender = "system", + content = "can't join mesh channels from here. switch to mesh chat first.", + timestamp = Date(), + isRelay = false + )) + return + } + + if (parts.size < 2) { + messageManager.addMessage(BitchatMessage( sender = "system", content = "usage: /join ", timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) + return + } + + val channelName = parts[1] + val channel = if (channelName.startsWith("#")) channelName else "#$channelName" + val password = if (parts.size > 2) parts[2] else null + + if (channelManager.joinChannel(channel, password, myPeerID)) { + messageManager.addMessage(BitchatMessage( + sender = "system", + content = "joined $channel", + timestamp = Date(), + isRelay = false + )) } } - private fun handleMessageCommand(parts: List, meshService: BluetoothMeshService) { - if (parts.size > 1) { - val targetName = parts[1].removePrefix("@") - val peerID = getPeerIDForNickname(targetName, meshService) - - if (peerID != null) { - val success = privateChatManager.startPrivateChat(peerID, meshService) - - if (success) { - if (parts.size > 2) { - val messageContent = parts.drop(2).joinToString(" ") - val recipientNickname = getPeerNickname(peerID, meshService) - privateChatManager.sendPrivateMessage( - messageContent, - peerID, - recipientNickname, - state.getNicknameValue(), - getMyPeerID(meshService) - ) { content, peerIdParam, recipientNicknameParam, messageId -> - // This would trigger the actual mesh service send - sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId) - } - } else { - val systemMessage = BitchatMessage( - sender = "system", - content = "started private chat with $targetName", - timestamp = Date(), - isRelay = false - ) - messageManager.addMessage(systemMessage) - } + private fun handleMessageCommand(parts: List, meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) { + if (parts.size < 2) { + messageManager.addMessage(BitchatMessage( + sender = "system", + content = "usage: /msg [message]", + timestamp = Date(), + isRelay = false + )) + return + } + + val targetName = parts[1].removePrefix("@") + + // Different peer lookup depending on whether we're in mesh or geohash context + val currentLocation = state.selectedLocationChannel.value + val peerID = try { + when { + currentLocation is com.bitchat.android.geohash.ChannelID.Location && viewModel != null -> { + // Geohash: find by display name in current participants + findGeohashPeerID(targetName, viewModel) } - } else { - val systemMessage = BitchatMessage( - sender = "system", - content = "user '$targetName' not found. they may be offline or using a different nickname.", - timestamp = Date(), - isRelay = false - ) - messageManager.addMessage(systemMessage) + else -> { + // Mesh: find by nickname in connected peers + getPeerIDForNickname(targetName, meshService) + } + } + } catch (e: Exception) { + null + } + + if (peerID == null) { + messageManager.addMessage(BitchatMessage( + sender = "system", + content = "couldn't find '$targetName'. they might be offline or using a different name.", + timestamp = Date(), + isRelay = false + )) + return + } + + if (!privateChatManager.startPrivateChat(peerID, meshService)) { + return + } + + // If there's a message to send immediately, send it + if (parts.size > 2) { + val messageContent = parts.drop(2).joinToString(" ") + val recipientNickname = resolveRecipientNickname(peerID, targetName, meshService, viewModel) + + privateChatManager.sendPrivateMessage( + messageContent, + peerID, + recipientNickname, + state.getNicknameValue(), + getMyPeerID(meshService) + ) { content, peerIdParam, recipientNicknameParam, messageId -> + sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId) } } else { - val systemMessage = BitchatMessage( + messageManager.addMessage(BitchatMessage( sender = "system", - content = "usage: /msg [message]", + content = "started chat with $targetName", timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) } } private fun handleWhoCommand(meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) { - // Channel-aware who command (matches iOS behavior) - val (peerList, contextDescription) = if (viewModel != null) { - when (val selectedChannel = viewModel.selectedLocationChannel.value) { - is com.bitchat.android.geohash.ChannelID.Mesh, - null -> { - // Mesh channel: show Bluetooth-connected peers - val connectedPeers = state.getConnectedPeersValue() - val peerList = connectedPeers.joinToString(", ") { peerID -> - getPeerNickname(peerID, meshService) - } - Pair(peerList, "online users") - } + val currentLocation = viewModel?.selectedLocationChannel?.value + + val (userList, context) = when (currentLocation) { + is com.bitchat.android.geohash.ChannelID.Location -> { + // Geohash location: show Nostr participants in this area + val people = viewModel.geohashPeople.value ?: emptyList() + val myNick = state.getNicknameValue() - is com.bitchat.android.geohash.ChannelID.Location -> { - // Location channel: show geohash participants - val geohashPeople = viewModel.geohashPeople.value ?: emptyList() - val currentNickname = state.getNicknameValue() - - val participantList = geohashPeople.mapNotNull { person -> - val displayName = person.displayName - // Exclude self from list - if (displayName.startsWith("${currentNickname}#")) { - null - } else { - displayName - } - }.joinToString(", ") - - Pair(participantList, "participants in ${selectedChannel.channel.geohash}") - } + val others = people + .map { it.displayName } + .filter { !it.startsWith("${myNick}#") } // Don't list ourselves + .joinToString(", ") + + Pair(others, "in ${currentLocation.channel.geohash}") } - } else { - // Fallback to mesh behavior - val connectedPeers = state.getConnectedPeersValue() - val peerList = connectedPeers.joinToString(", ") { peerID -> - getPeerNickname(peerID, meshService) + else -> { + // Mesh network: show Bluetooth-connected peers + val peers = state.getConnectedPeersValue() + val names = peers.joinToString(", ") { getPeerNickname(it, meshService) } + Pair(names, "online") } - Pair(peerList, "online users") } - val systemMessage = BitchatMessage( + val message = if (userList.isEmpty()) { + "nobody else around right now" + } else { + "$context: $userList" + } + + messageManager.addMessage(BitchatMessage( sender = "system", - content = if (peerList.isEmpty()) { - "no one else is around right now." - } else { - "$contextDescription: $peerList" - }, + content = message, timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) } private fun handleClearCommand() { + // Clear messages based on current view context when { state.getSelectedPrivateChatPeerValue() != null -> { - // Clear private chat val peerID = state.getSelectedPrivateChatPeerValue()!! messageManager.clearPrivateMessages(peerID) } state.getCurrentChannelValue() != null -> { - // Clear channel messages val channel = state.getCurrentChannelValue()!! messageManager.clearChannelMessages(channel) } + state.selectedLocationChannel.value is com.bitchat.android.geohash.ChannelID.Location -> { + val location = state.selectedLocationChannel.value as com.bitchat.android.geohash.ChannelID.Location + messageManager.clearChannelMessages("geo:${location.channel.geohash}") + } else -> { - // Clear main messages messageManager.clearMessages() } } @@ -206,78 +232,82 @@ class CommandProcessor( val currentChannel = state.getCurrentChannelValue() if (currentChannel == null) { - val systemMessage = BitchatMessage( + messageManager.addMessage(BitchatMessage( sender = "system", - content = "you must be in a channel to set a password.", + content = "need to be in a channel to set a password", timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) return } - if (parts.size == 2){ - if(!channelManager.isChannelCreator(channel = currentChannel, peerID = peerID)){ - val systemMessage = BitchatMessage( - sender = "system", - content = "you must be the channel creator to set a password.", - timestamp = Date(), - isRelay = false - ) - channelManager.addChannelMessage(currentChannel,systemMessage,null) - return - } - val newPassword = parts[1] - channelManager.setChannelPassword(currentChannel, newPassword) - val systemMessage = BitchatMessage( + if (parts.size != 2) { + channelManager.addChannelMessage(currentChannel, BitchatMessage( sender = "system", - content = "password changed for channel $currentChannel", + content = "usage: /pass ", timestamp = Date(), isRelay = false - ) - channelManager.addChannelMessage(currentChannel,systemMessage,null) + ), null) + return } - else{ - val systemMessage = BitchatMessage( + + if (!channelManager.isChannelCreator(currentChannel, peerID)) { + channelManager.addChannelMessage(currentChannel, BitchatMessage( sender = "system", - content = "usage: /pass ", + content = "only the channel creator can set a password", timestamp = Date(), isRelay = false - ) - channelManager.addChannelMessage(currentChannel,systemMessage,null) + ), null) + return } + + val newPassword = parts[1] + channelManager.setChannelPassword(currentChannel, newPassword) + channelManager.addChannelMessage(currentChannel, BitchatMessage( + sender = "system", + content = "password updated for $currentChannel", + timestamp = Date(), + isRelay = false + ), null) } - private fun handleBlockCommand(parts: List, meshService: BluetoothMeshService) { - if (parts.size > 1) { - val targetName = parts[1].removePrefix("@") - privateChatManager.blockPeerByNickname(targetName, meshService) - } else { - // List blocked users + private fun handleBlockCommand(parts: List, meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) { + if (parts.size < 2) { + // No target specified - list currently blocked users val blockedInfo = privateChatManager.listBlockedUsers() - val systemMessage = BitchatMessage( + messageManager.addMessage(BitchatMessage( sender = "system", content = blockedInfo, timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) + return + } + + val targetName = parts[1].removePrefix("@") + val currentLocation = state.selectedLocationChannel.value + + // Different blocking mechanisms for mesh vs geohash + if (currentLocation is com.bitchat.android.geohash.ChannelID.Location && viewModel != null) { + viewModel.blockUserInGeohash(targetName) + } else { + privateChatManager.blockPeerByNickname(targetName, meshService) } } private fun handleUnblockCommand(parts: List, meshService: BluetoothMeshService) { - if (parts.size > 1) { - val targetName = parts[1].removePrefix("@") - privateChatManager.unblockPeerByNickname(targetName, meshService) - } else { - val systemMessage = BitchatMessage( + if (parts.size < 2) { + messageManager.addMessage(BitchatMessage( sender = "system", content = "usage: /unblock ", timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) + return } + + val targetName = parts[1].removePrefix("@") + privateChatManager.unblockPeerByNickname(targetName, meshService) } private fun handleActionCommand( @@ -288,87 +318,97 @@ class CommandProcessor( myPeerID: String, onSendMessage: (String, List, String?) -> Unit ) { - if (parts.size > 1) { - val targetName = parts[1].removePrefix("@") - val actionMessage = "* ${state.getNicknameValue() ?: "someone"} $verb $targetName $object_ *" + if (parts.size < 2) { + messageManager.addMessage(BitchatMessage( + sender = "system", + content = "usage: /${parts[0].removePrefix("/")} ", + timestamp = Date(), + isRelay = false + )) + return + } + + val targetName = parts[1].removePrefix("@") + val myNick = state.getNicknameValue() ?: "someone" + val actionMessage = "* $myNick $verb $targetName $object_ *" - // If we're in a geohash location channel, don't add a local echo here. - // GeohashViewModel.sendGeohashMessage() will add the local echo with proper metadata. - val isInLocationChannel = state.selectedLocationChannel.value is com.bitchat.android.geohash.ChannelID.Location + val currentLocation = state.selectedLocationChannel.value + val currentChannel = state.getCurrentChannelValue() + val privatePeer = state.getSelectedPrivateChatPeerValue() - // Send as regular message - if (state.getSelectedPrivateChatPeerValue() != null) { - val peerID = state.getSelectedPrivateChatPeerValue()!! + when { + privatePeer != null -> { + // Send action to private chat privateChatManager.sendPrivateMessage( actionMessage, - peerID, - getPeerNickname(peerID, meshService), + privatePeer, + getPeerNickname(privatePeer, meshService), state.getNicknameValue(), myPeerID ) { content, peerIdParam, recipientNicknameParam, messageId -> sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId) } - } else if (isInLocationChannel) { - // Let the transport layer add the echo; just send it out + } + currentLocation is com.bitchat.android.geohash.ChannelID.Location -> { + // Geohash: let transport layer handle echo onSendMessage(actionMessage, emptyList(), null) - } else { + } + currentChannel != null -> { + // Mesh channel: add local echo and send val message = BitchatMessage( - sender = state.getNicknameValue() ?: myPeerID, + sender = myNick, content = actionMessage, timestamp = Date(), isRelay = false, senderPeerID = myPeerID, - channel = state.getCurrentChannelValue() + channel = currentChannel ) - - if (state.getCurrentChannelValue() != null) { - channelManager.addChannelMessage(state.getCurrentChannelValue()!!, message, myPeerID) - onSendMessage(actionMessage, emptyList(), state.getCurrentChannelValue()) - } else { - messageManager.addMessage(message) - onSendMessage(actionMessage, emptyList(), null) - } + channelManager.addChannelMessage(currentChannel, message, myPeerID) + onSendMessage(actionMessage, emptyList(), currentChannel) + } + else -> { + // Main mesh chat: add local echo and send + val message = BitchatMessage( + sender = myNick, + content = actionMessage, + timestamp = Date(), + isRelay = false, + senderPeerID = myPeerID, + channel = null + ) + messageManager.addMessage(message) + onSendMessage(actionMessage, emptyList(), null) } - } else { - val systemMessage = BitchatMessage( - sender = "system", - content = "usage: /${parts[0].removePrefix("/")} ", - timestamp = Date(), - isRelay = false - ) - messageManager.addMessage(systemMessage) } } private fun handleChannelsCommand() { - val allChannels = channelManager.getJoinedChannelsList() - val channelList = if (allChannels.isEmpty()) { - "no channels joined" + val channels = channelManager.getJoinedChannelsList() + val message = if (channels.isEmpty()) { + "not in any channels" } else { - "joined channels: ${allChannels.joinToString(", ")}" + "channels: ${channels.joinToString(", ")}" } - val systemMessage = BitchatMessage( + messageManager.addMessage(BitchatMessage( sender = "system", - content = channelList, + content = message, timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) } private fun handleUnknownCommand(cmd: String) { - val systemMessage = BitchatMessage( + messageManager.addMessage(BitchatMessage( sender = "system", content = "unknown command: $cmd. type / to see available commands.", timestamp = Date(), isRelay = false - ) - messageManager.addMessage(systemMessage) + )) } - // MARK: - Command Autocomplete - + // Command autocomplete + fun updateCommandSuggestions(input: String) { if (!input.startsWith("/")) { state.setShowCommandSuggestions(false) @@ -376,14 +416,11 @@ class CommandProcessor( return } - // Get all available commands based on context - val allCommands = getAllAvailableCommands() - - // Filter commands based on input - val filteredCommands = filterCommands(allCommands, input.lowercase()) + val available = getAllAvailableCommands() + val matching = filterCommands(available, input.lowercase()) - if (filteredCommands.isNotEmpty()) { - state.setCommandSuggestions(filteredCommands) + if (matching.isNotEmpty()) { + state.setCommandSuggestions(matching) state.setShowCommandSuggestions(true) } else { state.setShowCommandSuggestions(false) @@ -392,8 +429,7 @@ class CommandProcessor( } private fun getAllAvailableCommands(): List { - // Add channel-specific commands if in a channel - val channelCommands = if (state.getCurrentChannelValue() != null) { + val channelSpecific = if (state.getCurrentChannelValue() != null) { listOf( CommandSuggestion("/pass", emptyList(), "[password]", "change channel password"), CommandSuggestion("/save", emptyList(), null, "save channel messages locally"), @@ -403,15 +439,12 @@ class CommandProcessor( emptyList() } - return baseCommands + channelCommands + return baseCommands + channelSpecific } private fun filterCommands(commands: List, input: String): List { - return commands.filter { command -> - // Check primary command - command.command.startsWith(input) || - // Check aliases - command.aliases.any { it.startsWith(input) } + return commands.filter { cmd -> + cmd.command.startsWith(input) || cmd.aliases.any { it.startsWith(input) } }.sortedBy { it.command } } @@ -421,10 +454,9 @@ class CommandProcessor( return "${suggestion.command} " } - // MARK: - Mention Autocomplete + // Mention autocomplete (@nickname) fun updateMentionSuggestions(input: String, meshService: BluetoothMeshService, viewModel: ChatViewModel? = null) { - // Check if input contains @ and we're at the end of a word or at the end of input val atIndex = input.lastIndexOf('@') if (atIndex == -1) { state.setShowMentionSuggestions(false) @@ -432,53 +464,42 @@ class CommandProcessor( return } - // Get the text after the @ symbol val textAfterAt = input.substring(atIndex + 1) - // If there's a space after @, don't show suggestions + // Stop suggesting if there's a space after the @ if (textAfterAt.contains(' ')) { state.setShowMentionSuggestions(false) state.setMentionSuggestions(emptyList()) return } - // Get peer candidates based on active channel (matches iOS logic exactly) - val peerCandidates: List = if (viewModel != null) { - when (val selectedChannel = viewModel.selectedLocationChannel.value) { - is com.bitchat.android.geohash.ChannelID.Mesh, - null -> { - // Mesh channel: use Bluetooth mesh peer nicknames - meshService.getPeerNicknames().values.filter { it != meshService.getPeerNicknames()[meshService.myPeerID] } + // Get list of mentionable users based on current context + val currentLocation = viewModel?.selectedLocationChannel?.value + val candidates = when (currentLocation) { + is com.bitchat.android.geohash.ChannelID.Location -> { + // Geohash: use display names from participants + val people = viewModel.geohashPeople.value ?: emptyList() + val myNick = state.getNicknameValue() + people.mapNotNull { person -> + val name = person.displayName + if (name.startsWith("${myNick}#")) null else name } - - is com.bitchat.android.geohash.ChannelID.Location -> { - // Location channel: use geohash participants with collision-resistant suffixes - val geohashPeople = viewModel.geohashPeople.value ?: emptyList() - val currentNickname = state.getNicknameValue() - - geohashPeople.mapNotNull { person -> - val displayName = person.displayName - // Exclude self from suggestions - if (displayName.startsWith("${currentNickname}#")) { - null - } else { - displayName - } - } + } + else -> { + // Mesh: use peer nicknames + val myPeerID = meshService.myPeerID + meshService.getPeerNicknames().values.filter { + it != meshService.getPeerNicknames()[myPeerID] } } - } else { - // Fallback to mesh peers if no viewModel available - meshService.getPeerNicknames().values.filter { it != meshService.getPeerNicknames()[meshService.myPeerID] } } - // Filter nicknames based on the text after @ - val filteredNicknames = peerCandidates.filter { nickname -> - nickname.startsWith(textAfterAt, ignoreCase = true) - }.sorted() + val matching = candidates + .filter { it.startsWith(textAfterAt, ignoreCase = true) } + .sorted() - if (filteredNicknames.isNotEmpty()) { - state.setMentionSuggestions(filteredNicknames) + if (matching.isNotEmpty()) { + state.setMentionSuggestions(matching) state.setShowMentionSuggestions(true) } else { state.setShowMentionSuggestions(false) @@ -490,18 +511,60 @@ class CommandProcessor( state.setShowMentionSuggestions(false) state.setMentionSuggestions(emptyList()) - // Find the last @ symbol position val atIndex = currentText.lastIndexOf('@') if (atIndex == -1) { return "$currentText@$nickname " } - // Replace the text from the @ symbol to the end with the mention val textBeforeAt = currentText.substring(0, atIndex) return "$textBeforeAt@$nickname " } - // MARK: - Utility Functions + // Helper functions for peer lookup and messaging + + /** + * Find a geohash peer ID by their display name. + * Uses full 32-char Nostr ID to avoid collisions. + */ + private fun findGeohashPeerID(targetName: String, viewModel: ChatViewModel): String? { + val people = viewModel.geohashPeople.value ?: return null + + val match = people.find { person -> + val (baseName, _) = try { + com.bitchat.android.ui.splitSuffix(person.displayName) + } catch (e: Exception) { + return@find false + } + baseName.equals(targetName, ignoreCase = true) + } ?: return null + + val idLength = minOf(match.id.length, NOSTR_ID_LENGTH) + return "$NOSTR_PEER_PREFIX${match.id.take(idLength)}" + } + + /** + * Resolve the display nickname for a recipient. + * Handles both mesh peers and Nostr conversation keys. + */ + private fun resolveRecipientNickname( + peerID: String, + fallbackName: String, + meshService: BluetoothMeshService, + viewModel: ChatViewModel? + ): String { + return when { + peerID.startsWith(NOSTR_PEER_PREFIX) && viewModel != null -> { + // Nostr peer: look up display name from geohash people + val pubkeyHex = peerID.removePrefix(NOSTR_PEER_PREFIX) + val people = viewModel.geohashPeople.value ?: emptyList() + people.find { it.id.startsWith(pubkeyHex) }?.displayName ?: fallbackName + } + else -> { + // Mesh peer: use nickname from mesh service + getPeerNickname(peerID, meshService) + } + } + } private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? { return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key