Wire the Blossom MediaTransport into publishing: shareable lots carry their varietyId, publishLots uploads the lot's cover photo and puts the returned URL on the offer (best-effort — a hosting failure still publishes the offer, just without a photo). Add a configurable media server (Comunes default, empty to opt out) to settings and the market advanced config.
101 lines
4.5 KiB
Dart
101 lines
4.5 KiB
Dart
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';
|
|
static const _mediaServerKey = 'tane.social.media_server';
|
|
|
|
/// 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<String> defaultRelays = [
|
|
'wss://relay.comunes.org',
|
|
'wss://nos.lol',
|
|
'wss://relay.damus.io',
|
|
'wss://relay.primal.net',
|
|
];
|
|
|
|
/// Blossom media server used to host offer photos so peers can see them. The
|
|
/// Comunes-run server is the non-commercial default; the user can point this
|
|
/// at any Blossom server, or clear it to publish offers without photos.
|
|
static const String defaultMediaServer = 'https://blossom.comunes.org';
|
|
|
|
/// 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<String> areaGeohash() async => (await _store.read(_areaKey)) ?? '';
|
|
|
|
Future<void> 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<List<String>> 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<void> setRelayUrls(List<String> 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<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)}');
|
|
|
|
/// The Blossom media server for offer photos. Falls back to
|
|
/// [defaultMediaServer] until the user configures one; an explicitly saved
|
|
/// empty value turns photo hosting off (offers still publish, without images).
|
|
Future<String> mediaServerUrl() async {
|
|
final raw = await _store.read(_mediaServerKey);
|
|
if (raw == null) return defaultMediaServer; // never configured
|
|
return raw.trim();
|
|
}
|
|
|
|
Future<void> setMediaServerUrl(String url) =>
|
|
_store.write(_mediaServerKey, url.trim());
|
|
|
|
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;
|
|
}
|