import '../security/secret_store.dart'; /// User choices for the (Block 2) social layer: the coarse area to publish/ /// browse offers in, and which community relays to talk to. Backed by the OS /// keystore (via [SecretStore]) to honour "no plaintext at rest" — no /// shared_preferences. /// /// Relays default to a small set of well-known public servers so the market /// works out of the box; the exposure is minimal (offers are opt-in and carry /// only a coarse geohash) and the user can swap them for a community server. /// The area stays unset until the user picks one (it's inherently personal). class SocialSettings { SocialSettings(this._store); final SecretStore _store; 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. The Comunes /// relay comes first as the reliable, non-commercial home; the public ones /// are backup. static const List defaultRelays = [ 'wss://relay.comunes.org', 'wss://nos.lol', 'wss://relay.damus.io', 'wss://relay.primal.net', ]; /// The user's coarse area as a low-precision geohash (may be empty). Set /// manually or from device location; the transport coarsens it further. Future areaGeohash() async => (await _store.read(_areaKey)) ?? ''; Future setAreaGeohash(String geohash) => _store.write(_areaKey, geohash.trim().toLowerCase()); /// The relays to talk to. When the user has never configured any, falls back /// to [defaultRelays]. Once they save a choice (even an empty one), that wins /// — so a privacy-minded user can turn the network off entirely. Future> relayUrls() async { final raw = await _store.read(_relaysKey); if (raw == null) return const [...defaultRelays]; // never configured return raw.split('\n').where((s) => s.trim().isNotEmpty).toList(); } Future setRelayUrls(List urls) => _store.write( _relaysKey, 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 searchPrecision() async { final raw = await _store.read(_searchPrecisionKey); final value = int.tryParse(raw ?? ''); if (value == null) return defaultSearchPrecision; return _clampPrecision(value); } Future 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 get isConfigured async => (await relayUrls()).isNotEmpty && (await areaGeohash()).isNotEmpty; }