fix(market): search your zone by a coarse prefix, not the exact cell
Discovery searched the full 5-char geohash (~2.4 km) with an exact Nostr tag match, so two devices in adjacent cells never found each other even though publish emits the full NIP-52 prefix ladder. Search now coarsens the area to a configurable precision (SocialSettings.searchPrecision, default 4 ~20 km) via searchPrefix(), with a human-worded 'how far' selector (Very close / Around here / My region -> 5/4/3) in the market setup sheet. Also: surface a clear 'couldn't reach the servers' message when no relay accepts a share (instead of 'shared 0'), and add wss://relay.comunes.org as the first default relay (reliable community home; public relays are backup). Tests: settings precision (default/clamp/roundtrip), searchPrefix helper, neighbour-cell discovery regression (offers_cubit + commons_core nostr transports), and the range-selector widget test.
This commit is contained in:
parent
5dd0f243ce
commit
1af0282e00
16 changed files with 268 additions and 10 deletions
|
|
@ -370,7 +370,12 @@
|
|||
"nothingToShare": "Marca primero dalguna simiente pa regalar, trocar o vender",
|
||||
"useLocation": "Usar el mio allugamientu averáu",
|
||||
"locationFailed": "Nun se pudo consiguir el to allugamientu — comprueba que l'allugamientu ta activáu y el permisu concedíu",
|
||||
"queued": "Guardáu — compartirémosles cuando tengas conexón"
|
||||
"queued": "Guardáu — compartirémosles cuando tengas conexón",
|
||||
"shareFailed": "Nun se pudo coneutar colos sirvidores — les tos granes nun se compartieron. Vuelvi a intentalo nun momentu.",
|
||||
"rangeLabel": "Hasta ónde guetar",
|
||||
"rangeNear": "Bien cerca",
|
||||
"rangeArea": "Pela mio zona",
|
||||
"rangeRegion": "La mio rexón"
|
||||
},
|
||||
"profile": {
|
||||
"title": "El to perfil",
|
||||
|
|
|
|||
|
|
@ -373,7 +373,12 @@
|
|||
"nothingToShare": "Mark some seeds to give, swap or sell first",
|
||||
"useLocation": "Use my approximate location",
|
||||
"locationFailed": "Couldn't get your location — check that location is on and the permission is granted",
|
||||
"queued": "Saved — we'll share these when you're connected"
|
||||
"queued": "Saved — we'll share these when you're connected",
|
||||
"shareFailed": "Couldn't reach the community servers — your seeds weren't shared. Try again in a moment.",
|
||||
"rangeLabel": "How far to look",
|
||||
"rangeNear": "Very close",
|
||||
"rangeArea": "Around here",
|
||||
"rangeRegion": "My region"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Your profile",
|
||||
|
|
|
|||
|
|
@ -372,7 +372,12 @@
|
|||
"nothingToShare": "Marca antes algunas semillas para regalar, cambiar o vender",
|
||||
"useLocation": "Usar mi ubicación aproximada",
|
||||
"locationFailed": "No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido",
|
||||
"queued": "Guardado — las compartiremos cuando tengas conexión"
|
||||
"queued": "Guardado — las compartiremos cuando tengas conexión",
|
||||
"shareFailed": "No se pudo conectar con los servidores — tus semillas no se compartieron. Inténtalo de nuevo en un momento.",
|
||||
"rangeLabel": "Hasta dónde buscar",
|
||||
"rangeNear": "Muy cerca",
|
||||
"rangeArea": "Por mi zona",
|
||||
"rangeRegion": "Mi región"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Tu perfil",
|
||||
|
|
|
|||
|
|
@ -369,7 +369,12 @@
|
|||
"nothingToShare": "Marca primeiro algumas sementes para dar, trocar ou vender",
|
||||
"useLocation": "Usar a minha localização aproximada",
|
||||
"locationFailed": "Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida",
|
||||
"queued": "Guardado — vamos partilhá-las quando tiveres ligação"
|
||||
"queued": "Guardado — vamos partilhá-las quando tiveres ligação",
|
||||
"shareFailed": "Não foi possível contactar os servidores — as tuas sementes não foram partilhadas. Tenta de novo daqui a pouco.",
|
||||
"rangeLabel": "Até onde procurar",
|
||||
"rangeNear": "Muito perto",
|
||||
"rangeArea": "Pela minha zona",
|
||||
"rangeRegion": "A minha região"
|
||||
},
|
||||
"profile": {
|
||||
"title": "O teu perfil",
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 4
|
||||
/// Strings: 1444 (361 per locale)
|
||||
/// Strings: 1464 (366 per locale)
|
||||
///
|
||||
/// Built on 2026-07-10 at 13:48 UTC
|
||||
/// Built on 2026-07-10 at 13:52 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,21 @@ class Translations$market$en {
|
|||
|
||||
/// en: 'Saved — we'll share these when you're connected'
|
||||
String get queued => 'Saved — we\'ll share these when you\'re connected';
|
||||
|
||||
/// en: 'Couldn't reach the community servers — your seeds weren't shared. Try again in a moment.'
|
||||
String get shareFailed => 'Couldn\'t reach the community servers — your seeds weren\'t shared. Try again in a moment.';
|
||||
|
||||
/// en: 'How far to look'
|
||||
String get rangeLabel => 'How far to look';
|
||||
|
||||
/// en: 'Very close'
|
||||
String get rangeNear => 'Very close';
|
||||
|
||||
/// en: 'Around here'
|
||||
String get rangeArea => 'Around here';
|
||||
|
||||
/// en: 'My region'
|
||||
String get rangeRegion => 'My region';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -2096,6 +2111,11 @@ extension on Translations {
|
|||
'market.useLocation' => 'Use my approximate location',
|
||||
'market.locationFailed' => 'Couldn\'t get your location — check that location is on and the permission is granted',
|
||||
'market.queued' => 'Saved — we\'ll share these when you\'re connected',
|
||||
'market.shareFailed' => 'Couldn\'t reach the community servers — your seeds weren\'t shared. Try again in a moment.',
|
||||
'market.rangeLabel' => 'How far to look',
|
||||
'market.rangeNear' => 'Very close',
|
||||
'market.rangeArea' => 'Around here',
|
||||
'market.rangeRegion' => 'My region',
|
||||
'profile.title' => 'Your profile',
|
||||
'profile.name' => 'Display name',
|
||||
'profile.nameHint' => 'How others see you',
|
||||
|
|
|
|||
|
|
@ -647,6 +647,11 @@ class _Translations$market$es extends Translations$market$en {
|
|||
@override String get useLocation => 'Usar mi ubicación aproximada';
|
||||
@override String get locationFailed => 'No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido';
|
||||
@override String get queued => 'Guardado — las compartiremos cuando tengas conexión';
|
||||
@override String get shareFailed => 'No se pudo conectar con los servidores — tus semillas no se compartieron. Inténtalo de nuevo en un momento.';
|
||||
@override String get rangeLabel => 'Hasta dónde buscar';
|
||||
@override String get rangeNear => 'Muy cerca';
|
||||
@override String get rangeArea => 'Por mi zona';
|
||||
@override String get rangeRegion => 'Mi región';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -1388,6 +1393,11 @@ extension on TranslationsEs {
|
|||
'market.useLocation' => 'Usar mi ubicación aproximada',
|
||||
'market.locationFailed' => 'No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido',
|
||||
'market.queued' => 'Guardado — las compartiremos cuando tengas conexión',
|
||||
'market.shareFailed' => 'No se pudo conectar con los servidores — tus semillas no se compartieron. Inténtalo de nuevo en un momento.',
|
||||
'market.rangeLabel' => 'Hasta dónde buscar',
|
||||
'market.rangeNear' => 'Muy cerca',
|
||||
'market.rangeArea' => 'Por mi zona',
|
||||
'market.rangeRegion' => 'Mi región',
|
||||
'profile.title' => 'Tu perfil',
|
||||
'profile.name' => 'Nombre',
|
||||
'profile.nameHint' => 'Cómo te ven los demás',
|
||||
|
|
|
|||
|
|
@ -644,6 +644,11 @@ class _Translations$market$pt extends Translations$market$en {
|
|||
@override String get useLocation => 'Usar a minha localização aproximada';
|
||||
@override String get locationFailed => 'Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida';
|
||||
@override String get queued => 'Guardado — vamos partilhá-las quando tiveres ligação';
|
||||
@override String get shareFailed => 'Não foi possível contactar os servidores — as tuas sementes não foram partilhadas. Tenta de novo daqui a pouco.';
|
||||
@override String get rangeLabel => 'Até onde procurar';
|
||||
@override String get rangeNear => 'Muito perto';
|
||||
@override String get rangeArea => 'Pela minha zona';
|
||||
@override String get rangeRegion => 'A minha região';
|
||||
}
|
||||
|
||||
// Path: profile
|
||||
|
|
@ -1382,6 +1387,11 @@ extension on TranslationsPt {
|
|||
'market.useLocation' => 'Usar a minha localização aproximada',
|
||||
'market.locationFailed' => 'Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida',
|
||||
'market.queued' => 'Guardado — vamos partilhá-las quando tiveres ligação',
|
||||
'market.shareFailed' => 'Não foi possível contactar os servidores — as tuas sementes não foram partilhadas. Tenta de novo daqui a pouco.',
|
||||
'market.rangeLabel' => 'Até onde procurar',
|
||||
'market.rangeNear' => 'Muito perto',
|
||||
'market.rangeArea' => 'Pela minha zona',
|
||||
'market.rangeRegion' => 'A minha região',
|
||||
'profile.title' => 'O teu perfil',
|
||||
'profile.name' => 'Nome',
|
||||
'profile.nameHint' => 'Como os outros te veem',
|
||||
|
|
|
|||
11
apps/app_seeds/lib/services/discovery_area.dart
Normal file
11
apps/app_seeds/lib/services/discovery_area.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/// The geohash prefix to search offers by, given the user's coarse [area] and
|
||||
/// the chosen [precision] (a prefix length; see
|
||||
/// `SocialSettings.searchPrecision`).
|
||||
///
|
||||
/// Publishing emits a NIP-52 prefix ladder from the (already coarse) area, so a
|
||||
/// SHORTER search prefix still matches finer offers. Searching at the full area
|
||||
/// length only matches the exact ±2.4 km cell — which is why two neighbours in
|
||||
/// adjacent cells used to find nothing. We recorten the area to [precision] (but
|
||||
/// never beyond its own length) so "your zone" is genuinely a zone, not a point.
|
||||
String searchPrefix(String area, int precision) =>
|
||||
area.substring(0, area.length < precision ? area.length : precision);
|
||||
|
|
@ -16,11 +16,24 @@ class SocialSettings {
|
|||
|
||||
static const _areaKey = 'tane.social.area_geohash';
|
||||
static const _relaysKey = 'tane.social.relays';
|
||||
static const _searchPrecisionKey = 'tane.social.search_precision';
|
||||
|
||||
/// How wide "your zone" searches, as a geohash prefix length: 5 ≈ ±2.4 km
|
||||
/// ("very close"), 4 ≈ ±20 km ("around here"), 3 ≈ ±78 km ("my region").
|
||||
/// Publishing always emits the full prefix ladder, so a coarser search still
|
||||
/// matches finer offers — the default is deliberately wide so a still-sparse
|
||||
/// network shows something.
|
||||
static const int minSearchPrecision = 3;
|
||||
static const int maxSearchPrecision = 5;
|
||||
static const int defaultSearchPrecision = 4;
|
||||
|
||||
/// Community servers used automatically so sharing works from the first
|
||||
/// launch. The relay pool skips any that are unreachable, so a dead one never
|
||||
/// breaks the market; the user never has to know these exist.
|
||||
/// breaks the market; the user never has to know these exist. The Comunes
|
||||
/// relay comes first as the reliable, non-commercial home; the public ones
|
||||
/// are backup.
|
||||
static const List<String> defaultRelays = [
|
||||
'wss://relay.comunes.org',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.primal.net',
|
||||
|
|
@ -47,6 +60,23 @@ class SocialSettings {
|
|||
urls.map((u) => u.trim()).where((u) => u.isNotEmpty).join('\n'),
|
||||
);
|
||||
|
||||
/// How wide to search — a geohash prefix length in [minSearchPrecision,
|
||||
/// maxSearchPrecision]. Defaults (and falls back on any garbage) to
|
||||
/// [defaultSearchPrecision].
|
||||
Future<int> searchPrecision() async {
|
||||
final raw = await _store.read(_searchPrecisionKey);
|
||||
final value = int.tryParse(raw ?? '');
|
||||
if (value == null) return defaultSearchPrecision;
|
||||
return _clampPrecision(value);
|
||||
}
|
||||
|
||||
Future<void> setSearchPrecision(int precision) =>
|
||||
_store.write(_searchPrecisionKey, '${_clampPrecision(precision)}');
|
||||
|
||||
int _clampPrecision(int p) => p < minSearchPrecision
|
||||
? minSearchPrecision
|
||||
: (p > maxSearchPrecision ? maxSearchPrecision : p);
|
||||
|
||||
/// Whether the social layer has enough config to attempt going online.
|
||||
Future<bool> get isConfigured async =>
|
||||
(await relayUrls()).isNotEmpty && (await areaGeohash()).isNotEmpty;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
|||
import '../data/variety_repository.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/coarse_location.dart';
|
||||
import '../services/discovery_area.dart';
|
||||
import '../services/offer_outbox.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
|
|
@ -81,7 +82,10 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
areaGeohash: area,
|
||||
);
|
||||
}
|
||||
if (mounted) await cubit.discover(area);
|
||||
if (mounted) {
|
||||
final precision = await widget.settings.searchPrecision();
|
||||
if (mounted) await cubit.discover(searchPrefix(area, precision));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,8 +136,14 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
);
|
||||
if (!mounted) return;
|
||||
await widget.outbox?.remove(ids); // published now, so unpark any duplicates
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.market.sharedCount(n: count))));
|
||||
await cubit.discover(area); // refresh — now including the just-shared lots
|
||||
// count == 0 with lots to share means every relay refused — say so plainly
|
||||
// rather than "shared 0", which reads like success.
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text(count == 0 ? t.market.shareFailed : t.market.sharedCount(n: count)),
|
||||
));
|
||||
final precision = await widget.settings.searchPrecision();
|
||||
if (!mounted) return;
|
||||
await cubit.discover(searchPrefix(area, precision)); // refresh incl. just-shared
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -527,6 +537,7 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
bool _loading = true;
|
||||
bool _locationBusy = false;
|
||||
String? _locationError;
|
||||
int _precision = SocialSettings.defaultSearchPrecision;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -537,12 +548,14 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
Future<void> _load() async {
|
||||
_area.text = await widget.settings.areaGeohash();
|
||||
_servers.text = (await widget.settings.relayUrls()).join('\n');
|
||||
_precision = await widget.settings.searchPrecision();
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
await widget.settings.setAreaGeohash(_area.text);
|
||||
await widget.settings.setRelayUrls(_servers.text.split('\n'));
|
||||
await widget.settings.setSearchPrecision(_precision);
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
|
|
@ -654,6 +667,27 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
t.market.rangeLabel,
|
||||
style: const TextStyle(fontSize: 13, color: seedMuted),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SegmentedButton<int>(
|
||||
key: const Key('market.range'),
|
||||
segments: [
|
||||
ButtonSegment(value: 5, label: Text(t.market.rangeNear)),
|
||||
ButtonSegment(value: 4, label: Text(t.market.rangeArea)),
|
||||
ButtonSegment(value: 3, label: Text(t.market.rangeRegion)),
|
||||
],
|
||||
selected: {_precision},
|
||||
showSelectedIcon: false,
|
||||
onSelectionChanged: (s) =>
|
||||
setState(() => _precision = s.first),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Theme(
|
||||
data: Theme.of(context)
|
||||
|
|
|
|||
22
apps/app_seeds/test/services/discovery_area_test.dart
Normal file
22
apps/app_seeds/test/services/discovery_area_test.dart
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/services/discovery_area.dart';
|
||||
|
||||
void main() {
|
||||
test('coarsens the area to the search precision', () {
|
||||
expect(searchPrefix('sp3e9', 4), 'sp3e');
|
||||
expect(searchPrefix('sp3e9', 3), 'sp3');
|
||||
expect(searchPrefix('sp3e9', 5), 'sp3e9');
|
||||
});
|
||||
|
||||
test('never asks for more than the area actually has', () {
|
||||
expect(searchPrefix('sp', 4), 'sp');
|
||||
expect(searchPrefix('', 4), '');
|
||||
});
|
||||
|
||||
test('two neighbours in adjacent 5-char cells share a 4-char prefix', () {
|
||||
// The crux of the bug: sp3e9 and sp3e8 are different ±2.4 km cells, but
|
||||
// searching at precision 4 collapses both to 'sp3e', so they find each
|
||||
// other. Publishing emits the ladder down to 'sp3e', so this matches.
|
||||
expect(searchPrefix('sp3e9', 4), searchPrefix('sp3e8', 4));
|
||||
});
|
||||
}
|
||||
|
|
@ -47,4 +47,28 @@ void main() {
|
|||
await settings.setAreaGeohash('sp3e9');
|
||||
expect(await settings.isConfigured, isTrue);
|
||||
});
|
||||
|
||||
test('search precision defaults to 4 (a wide "around here")', () async {
|
||||
expect(await settings.searchPrecision(), 4);
|
||||
});
|
||||
|
||||
test('stores and reads back the search precision', () async {
|
||||
await settings.setSearchPrecision(3);
|
||||
expect(await settings.searchPrecision(), 3);
|
||||
await settings.setSearchPrecision(5);
|
||||
expect(await settings.searchPrecision(), 5);
|
||||
});
|
||||
|
||||
test('search precision is clamped to the 3–5 range', () async {
|
||||
await settings.setSearchPrecision(1);
|
||||
expect(await settings.searchPrecision(), 3);
|
||||
await settings.setSearchPrecision(9);
|
||||
expect(await settings.searchPrecision(), 5);
|
||||
});
|
||||
|
||||
test('a corrupt stored precision falls back to the default', () async {
|
||||
final store = FakeSecretStore();
|
||||
await store.write('tane.social.search_precision', 'oops');
|
||||
expect(await SocialSettings(store).searchPrecision(), 4);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:commons_core/commons_core.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/data/variety_repository.dart';
|
||||
import 'package:tane/db/enums.dart';
|
||||
import 'package:tane/services/discovery_area.dart';
|
||||
import 'package:tane/services/offer_mapper.dart';
|
||||
import 'package:tane/services/offer_outbox.dart';
|
||||
import 'package:tane/state/offers_cubit.dart';
|
||||
|
|
@ -152,6 +153,31 @@ void main() {
|
|||
await cubit.close();
|
||||
});
|
||||
|
||||
test('a neighbour in an adjacent cell is found via the coarsened prefix',
|
||||
() async {
|
||||
// Regression: A publishes at 'sp3e9', B lives one ±2.4 km cell over at
|
||||
// 'sp3e8'. Searching B's full 5-char area misses it; searching the
|
||||
// precision-4 prefix ('sp3e') finds it, because publish emits the ladder.
|
||||
final transport = FakeOfferTransport();
|
||||
await transport.publish(OfferMapper.fromSharedLot(
|
||||
lotId: 'lot-1',
|
||||
authorPubkeyHex: 'ab' * 32,
|
||||
summary: 'Tomate rosa',
|
||||
sharing: OfferStatus.shared,
|
||||
areaGeohash: 'sp3e9',
|
||||
));
|
||||
final cubit = OffersCubit(transport);
|
||||
|
||||
await cubit.discover(searchPrefix('sp3e8', 4)); // 'sp3e'
|
||||
await pumpEventQueue();
|
||||
expect(cubit.state.offers, hasLength(1));
|
||||
|
||||
await cubit.discover('sp3e8'); // full precision → the old, broken behaviour
|
||||
await pumpEventQueue();
|
||||
expect(cubit.state.offers, isEmpty);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('a different area finds nothing', () async {
|
||||
final transport = FakeOfferTransport();
|
||||
await transport.publish(OfferMapper.fromSharedLot(
|
||||
|
|
|
|||
|
|
@ -91,6 +91,28 @@ void main() {
|
|||
expect(find.text('Retry'), findsOneWidget); // can't-reach state offers retry
|
||||
});
|
||||
|
||||
testWidgets('the range selector persists the chosen search precision',
|
||||
(tester) async {
|
||||
final social = await SocialService.fromRootSeedHex('00' * 32);
|
||||
final settings = SocialSettings(InMemorySecretStore());
|
||||
await settings.setRelayUrls(const []); // offline: don't hit the network
|
||||
|
||||
await tester.pumpWidget(_wrapMarket(social, settings));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byKey(const Key('market.config')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Defaults to the wide "Around here" (precision 4); narrow to "My region".
|
||||
expect(await settings.searchPrecision(), 4);
|
||||
await tester.tap(find.text('My region'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('market.save')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(await settings.searchPrecision(), 3);
|
||||
});
|
||||
|
||||
testWidgets('"use my location" fills the area with a coarse geohash',
|
||||
(tester) async {
|
||||
final social = await SocialService.fromRootSeedHex('00' * 32);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,35 @@ void main() {
|
|||
await bobT.close();
|
||||
});
|
||||
|
||||
test('a coarser search prefix matches, an adjacent full cell does not',
|
||||
() async {
|
||||
// Alice publishes at 'sp3e9'. A neighbour searching the precision-4
|
||||
// prefix 'sp3e' finds her (the publish ladder emits 'sp3e'); searching
|
||||
// the adjacent full cell 'sp3e8' finds nothing (exact tag match).
|
||||
final alice = await idFor(1);
|
||||
final bob = await idFor(2);
|
||||
final aliceT = NostrOfferTransport(await connFor(alice));
|
||||
final bobT = NostrOfferTransport(await connFor(bob));
|
||||
await aliceT.publish(Offer(
|
||||
id: 'tomate-1',
|
||||
authorPubkeyHex: alice.publicKeyHex,
|
||||
summary: 'Tomate rosa',
|
||||
type: OfferType.gift,
|
||||
approxGeohash: 'sp3e9',
|
||||
));
|
||||
|
||||
final wide = await bobT
|
||||
.discoverUntilEose(const DiscoveryQuery(geohashPrefix: 'sp3e'));
|
||||
expect(wide, hasLength(1));
|
||||
|
||||
final adjacent = await bobT
|
||||
.discoverUntilEose(const DiscoveryQuery(geohashPrefix: 'sp3e8'));
|
||||
expect(adjacent, isEmpty);
|
||||
|
||||
await aliceT.close();
|
||||
await bobT.close();
|
||||
});
|
||||
|
||||
test('exact geohash never leaves the device (coarsened on the wire)',
|
||||
() async {
|
||||
final alice = await idFor(1);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue