diff --git a/apps/app_seeds/lib/i18n/en.i18n.json b/apps/app_seeds/lib/i18n/en.i18n.json index 1ff23e4..f96a284 100644 --- a/apps/app_seeds/lib/i18n/en.i18n.json +++ b/apps/app_seeds/lib/i18n/en.i18n.json @@ -343,8 +343,9 @@ "empty": "No seeds shared near you yet", "near": "Near you", "contact": "Message", + "mine": "You", "configTitle": "Sharing setup", - "setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle. You can change the servers below.", + "setupIntro": "Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.", "areaLabel": "Your area", "areaHelp": "Kept rough on purpose — your zone, never an exact spot.", "serversLabel": "Community servers", diff --git a/apps/app_seeds/lib/i18n/es.i18n.json b/apps/app_seeds/lib/i18n/es.i18n.json index c0f4020..cb8043c 100644 --- a/apps/app_seeds/lib/i18n/es.i18n.json +++ b/apps/app_seeds/lib/i18n/es.i18n.json @@ -343,8 +343,9 @@ "empty": "Aún no hay semillas compartidas cerca de ti", "near": "Cerca de ti", "contact": "Mensaje", + "mine": "Tú", "configTitle": "Configuración de compartir", - "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio. Puedes cambiar los servidores abajo.", + "setupIntro": "Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.", "areaLabel": "Tu zona", "areaHelp": "Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.", "serversLabel": "Servidores de la comunidad", diff --git a/apps/app_seeds/lib/i18n/pt.i18n.json b/apps/app_seeds/lib/i18n/pt.i18n.json index ae2904a..34db278 100644 --- a/apps/app_seeds/lib/i18n/pt.i18n.json +++ b/apps/app_seeds/lib/i18n/pt.i18n.json @@ -339,8 +339,9 @@ "empty": "Ainda não há sementes partilhadas perto de ti", "near": "Perto de ti", "contact": "Mensagem", + "mine": "Tu", "configTitle": "Configuração de partilha", - "setupIntro": "Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio. Podes mudar os servidores abaixo.", + "setupIntro": "Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.", "areaLabel": "A tua zona", "areaHelp": "Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.", "serversLabel": "Servidores da comunidade", diff --git a/apps/app_seeds/lib/i18n/strings.g.dart b/apps/app_seeds/lib/i18n/strings.g.dart index b6c3c57..f904738 100644 --- a/apps/app_seeds/lib/i18n/strings.g.dart +++ b/apps/app_seeds/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 3 -/// Strings: 986 (328 per locale) +/// Strings: 989 (329 per locale) /// -/// Built on 2026-07-10 at 09:19 UTC +/// Built on 2026-07-10 at 09:34 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/app_seeds/lib/i18n/strings_en.g.dart b/apps/app_seeds/lib/i18n/strings_en.g.dart index f25181d..e3324e2 100644 --- a/apps/app_seeds/lib/i18n/strings_en.g.dart +++ b/apps/app_seeds/lib/i18n/strings_en.g.dart @@ -1090,11 +1090,14 @@ class Translations$market$en { /// en: 'Message' String get contact => 'Message'; + /// en: 'You' + String get mine => 'You'; + /// en: 'Sharing setup' String get configTitle => 'Sharing setup'; - /// en: 'Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle. You can change the servers below.' - String get setupIntro => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle. You can change the servers below.'; + /// en: 'Sharing with people nearby is optional. Just set your rough area — you're already connected to shared community servers so people can find what you offer, with no company in the middle.' + String get setupIntro => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle.'; /// en: 'Your area' String get areaLabel => 'Your area'; @@ -1946,8 +1949,9 @@ extension on Translations { 'market.empty' => 'No seeds shared near you yet', 'market.near' => 'Near you', 'market.contact' => 'Message', + 'market.mine' => 'You', 'market.configTitle' => 'Sharing setup', - 'market.setupIntro' => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle. You can change the servers below.', + 'market.setupIntro' => 'Sharing with people nearby is optional. Just set your rough area — you\'re already connected to shared community servers so people can find what you offer, with no company in the middle.', 'market.areaLabel' => 'Your area', 'market.areaHelp' => 'Kept rough on purpose — your zone, never an exact spot.', 'market.serversLabel' => 'Community servers', diff --git a/apps/app_seeds/lib/i18n/strings_es.g.dart b/apps/app_seeds/lib/i18n/strings_es.g.dart index 8fc52d0..2e1ecfa 100644 --- a/apps/app_seeds/lib/i18n/strings_es.g.dart +++ b/apps/app_seeds/lib/i18n/strings_es.g.dart @@ -615,8 +615,9 @@ class _Translations$market$es extends Translations$market$en { @override String get empty => 'Aún no hay semillas compartidas cerca de ti'; @override String get near => 'Cerca de ti'; @override String get contact => 'Mensaje'; + @override String get mine => 'Tú'; @override String get configTitle => 'Configuración de compartir'; - @override String get setupIntro => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio. Puedes cambiar los servidores abajo.'; + @override String get setupIntro => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.'; @override String get areaLabel => 'Tu zona'; @override String get areaHelp => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.'; @override String get serversLabel => 'Servidores de la comunidad'; @@ -1306,8 +1307,9 @@ extension on TranslationsEs { 'market.empty' => 'Aún no hay semillas compartidas cerca de ti', 'market.near' => 'Cerca de ti', 'market.contact' => 'Mensaje', + 'market.mine' => 'Tú', 'market.configTitle' => 'Configuración de compartir', - 'market.setupIntro' => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio. Puedes cambiar los servidores abajo.', + 'market.setupIntro' => 'Compartir con gente cercana es opcional. Solo indica tu zona aproximada — ya estás conectado a servidores comunitarios compartidos para que otras personas encuentren lo que ofreces, sin ninguna empresa en medio.', 'market.areaLabel' => 'Tu zona', 'market.areaHelp' => 'Se mantiene aproximado a propósito — tu zona, nunca un punto exacto.', 'market.serversLabel' => 'Servidores de la comunidad', diff --git a/apps/app_seeds/lib/i18n/strings_pt.g.dart b/apps/app_seeds/lib/i18n/strings_pt.g.dart index 1b2c27a..7f2b7d6 100644 --- a/apps/app_seeds/lib/i18n/strings_pt.g.dart +++ b/apps/app_seeds/lib/i18n/strings_pt.g.dart @@ -611,8 +611,9 @@ class _Translations$market$pt extends Translations$market$en { @override String get empty => 'Ainda não há sementes partilhadas perto de ti'; @override String get near => 'Perto de ti'; @override String get contact => 'Mensagem'; + @override String get mine => 'Tu'; @override String get configTitle => 'Configuração de partilha'; - @override String get setupIntro => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio. Podes mudar os servidores abaixo.'; + @override String get setupIntro => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.'; @override String get areaLabel => 'A tua zona'; @override String get areaHelp => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.'; @override String get serversLabel => 'Servidores da comunidade'; @@ -1298,8 +1299,9 @@ extension on TranslationsPt { 'market.empty' => 'Ainda não há sementes partilhadas perto de ti', 'market.near' => 'Perto de ti', 'market.contact' => 'Mensagem', + 'market.mine' => 'Tu', 'market.configTitle' => 'Configuração de partilha', - 'market.setupIntro' => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio. Podes mudar os servidores abaixo.', + 'market.setupIntro' => 'Partilhar com pessoas por perto é opcional. Basta indicar a tua zona aproximada — já estás ligado a servidores comunitários partilhados para que outras pessoas encontrem o que ofereces, sem nenhuma empresa no meio.', 'market.areaLabel' => 'A tua zona', 'market.areaHelp' => 'Mantém-se aproximado de propósito — a tua zona, nunca um ponto exato.', 'market.serversLabel' => 'Servidores da comunidade', diff --git a/apps/app_seeds/lib/services/social_settings.dart b/apps/app_seeds/lib/services/social_settings.dart index 85b634f..293c81b 100644 --- a/apps/app_seeds/lib/services/social_settings.dart +++ b/apps/app_seeds/lib/services/social_settings.dart @@ -1,12 +1,5 @@ import '../security/secret_store.dart'; -/// A suggested community server the user can join with one tap. -class RelayPreset { - const RelayPreset(this.name, this.url); - final String name; - final String url; -} - /// 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 @@ -24,17 +17,15 @@ class SocialSettings { static const _areaKey = 'tane.social.area_geohash'; static const _relaysKey = 'tane.social.relays'; - /// Suggested servers shown as one-tap options in the sharing setup. - static const List presets = [ - RelayPreset('nos.lol', 'wss://nos.lol'), - RelayPreset('Damus', 'wss://relay.damus.io'), - RelayPreset('Primal', 'wss://relay.primal.net'), + /// 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. + static const List defaultRelays = [ + 'wss://nos.lol', + 'wss://relay.damus.io', + 'wss://relay.primal.net', ]; - /// Applied automatically for new users (a subset of [presets]) so sharing - /// works from the first launch, until the user changes it. - static const List defaultRelays = ['wss://nos.lol', 'wss://relay.damus.io']; - /// 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)) ?? ''; diff --git a/apps/app_seeds/lib/ui/app_drawer.dart b/apps/app_seeds/lib/ui/app_drawer.dart index 9bf5bfb..f7f5285 100644 --- a/apps/app_seeds/lib/ui/app_drawer.dart +++ b/apps/app_seeds/lib/ui/app_drawer.dart @@ -11,7 +11,11 @@ import 'theme.dart'; /// chat…) belong to Block 2 and are greyed with a "soon" tag, and Settings sits /// pinned at the bottom. class AppDrawer extends StatelessWidget { - const AppDrawer({super.key}); + const AppDrawer({this.marketEnabled = false, super.key}); + + /// When the Block 2 social layer is wired, the Market becomes a live drawer + /// destination; other social items stay "soon" until they're built. + final bool marketEnabled; @override Widget build(BuildContext context) { @@ -34,6 +38,12 @@ class AppDrawer extends StatelessWidget { icon: const Icon(Symbols.storefront), label: t.menu.market, divider: true, + onTap: marketEnabled + ? () { + Navigator.of(context).pop(); + context.go('/market'); + } + : null, ), _DrawerItem(icon: const Icon(Icons.person), label: t.menu.profile), _DrawerItem( diff --git a/apps/app_seeds/lib/ui/home_screen.dart b/apps/app_seeds/lib/ui/home_screen.dart index 58826ce..a9c9612 100644 --- a/apps/app_seeds/lib/ui/home_screen.dart +++ b/apps/app_seeds/lib/ui/home_screen.dart @@ -22,7 +22,7 @@ class HomeScreen extends StatelessWidget { final t = context.t; return Scaffold( appBar: AppBar(title: Text(t.app.title)), - drawer: const AppDrawer(), + drawer: AppDrawer(marketEnabled: marketEnabled), body: Stack( children: [ const Positioned.fill(child: _SeedWatermark()), diff --git a/apps/app_seeds/lib/ui/market_screen.dart b/apps/app_seeds/lib/ui/market_screen.dart index 9fb9048..41a8906 100644 --- a/apps/app_seeds/lib/ui/market_screen.dart +++ b/apps/app_seeds/lib/ui/market_screen.dart @@ -228,11 +228,11 @@ class MarketBody extends StatelessWidget { separatorBuilder: (_, _) => const SizedBox(height: 12), itemBuilder: (context, i) { final o = state.offers[i]; + final mine = o.authorPubkeyHex == selfPubkey; return _OfferCard( offer: o, - onContact: o.authorPubkeyHex == selfPubkey - ? null - : () => context.go('/chat/${o.authorPubkeyHex}'), + mine: mine, + onContact: mine ? null : () => context.go('/chat/${o.authorPubkeyHex}'), ); }, ); @@ -244,10 +244,13 @@ class MarketBody extends StatelessWidget { /// One discovered offer. Human words for the reciprocity mode; coarse "near you" /// instead of any precise location; price only when it's a sale. class _OfferCard extends StatelessWidget { - const _OfferCard({required this.offer, this.onContact}); + const _OfferCard({required this.offer, this.mine = false, this.onContact}); final Offer offer; + /// Whether this is the current user's own listing (shown with a "You" badge). + final bool mine; + /// Opens a private chat with the author; null for our own listings. final VoidCallback? onContact; @@ -276,6 +279,24 @@ class _OfferCard extends StatelessWidget { ), ), ), + if (mine) ...[ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: seedGreen, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + t.market.mine, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 6), + ], _TypeChip(label: _offerTypeLabel(t, offer.type)), ], ), @@ -408,8 +429,6 @@ class _ConfigSheet extends StatefulWidget { class _ConfigSheetState extends State<_ConfigSheet> { final _area = TextEditingController(); - final _servers = TextEditingController(); - final Set _selectedRelays = {}; bool _loading = true; bool _locationBusy = false; String? _locationError; @@ -422,22 +441,13 @@ class _ConfigSheetState extends State<_ConfigSheet> { Future _load() async { _area.text = await widget.settings.areaGeohash(); - final current = await widget.settings.relayUrls(); - final presetUrls = SocialSettings.presets.map((p) => p.url).toSet(); - _selectedRelays - ..clear() - ..addAll(current.where(presetUrls.contains)); - // Any relays that aren't presets go in the "add another" field. - _servers.text = current.where((u) => !presetUrls.contains(u)).join('\n'); if (mounted) setState(() => _loading = false); } Future _save() async { + // Relays are managed automatically (default community servers, dead ones + // skipped by the pool) — the user only sets their area here. await widget.settings.setAreaGeohash(_area.text); - await widget.settings.setRelayUrls([ - ..._selectedRelays, - ..._servers.text.split('\n'), - ]); if (mounted) Navigator.of(context).pop(true); } @@ -465,7 +475,6 @@ class _ConfigSheetState extends State<_ConfigSheet> { @override void dispose() { _area.dispose(); - _servers.dispose(); super.dispose(); } @@ -533,64 +542,6 @@ class _ConfigSheetState extends State<_ConfigSheet> { style: const TextStyle(color: Color(0xFFB3261E), fontSize: 12), ), ), - const SizedBox(height: 16), - // Pick from a short list of shared servers — pre-selected so it - // works out of the box; no jargon-y blank field to puzzle over. - Text( - t.market.serversLabel, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: seedOnSurface, - ), - ), - Text( - t.market.serversHelp, - style: const TextStyle(color: seedMuted, fontSize: 12, height: 1.3), - ), - for (final preset in SocialSettings.presets) - CheckboxListTile( - key: Key('market.relay.${preset.name}'), - contentPadding: EdgeInsets.zero, - dense: true, - controlAffinity: ListTileControlAffinity.leading, - value: _selectedRelays.contains(preset.url), - title: Text(preset.name), - subtitle: Text(preset.url, - style: const TextStyle(fontSize: 11, color: seedMuted)), - onChanged: (checked) => setState(() { - if (checked ?? false) { - _selectedRelays.add(preset.url); - } else { - _selectedRelays.remove(preset.url); - } - }), - ), - // A rarely-needed escape hatch for a community's own server. - Theme( - data: Theme.of(context) - .copyWith(dividerColor: Colors.transparent), - child: ExpansionTile( - key: const Key('market.serversAdvanced'), - tilePadding: EdgeInsets.zero, - childrenPadding: const EdgeInsets.only(bottom: 8), - title: Text( - t.market.serversAdvanced, - style: const TextStyle(fontSize: 13, color: seedMuted), - ), - children: [ - TextField( - key: const Key('market.servers'), - controller: _servers, - minLines: 1, - maxLines: 3, - decoration: const InputDecoration( - hintText: 'wss://…', - ), - ), - ], - ), - ), const SizedBox(height: 20), FilledButton( key: const Key('market.save'), diff --git a/apps/app_seeds/test/ui/market_screen_test.dart b/apps/app_seeds/test/ui/market_screen_test.dart index 2a1ee5f..76dcfa2 100644 --- a/apps/app_seeds/test/ui/market_screen_test.dart +++ b/apps/app_seeds/test/ui/market_screen_test.dart @@ -48,7 +48,7 @@ class FakeOfferTransport implements OfferTransport { Future close() async {} } -Widget _wrapBody(OffersCubit cubit) { +Widget _wrapBody(OffersCubit cubit, {String? selfPubkey}) { LocaleSettings.setLocaleSync(AppLocale.en); return TranslationProvider( child: MaterialApp( @@ -62,7 +62,7 @@ Widget _wrapBody(OffersCubit cubit) { home: Scaffold( body: BlocProvider.value( value: cubit, - child: MarketBody(onConfigure: () {}), + child: MarketBody(onConfigure: () {}, selfPubkey: selfPubkey), ), ), ), @@ -78,6 +78,27 @@ Offer _offer(String id, String summary, OfferType type) => Offer( ); void main() { + testWidgets('shows a "You" badge on my own listing', (tester) async { + const me = 'aa'; + final cubit = OffersCubit(FakeOfferTransport([ + Offer( + id: '1', + authorPubkeyHex: me, + summary: 'Mine', + type: OfferType.gift, + approxGeohash: 'sp3e9', + ), + ])); + await tester.pumpWidget(_wrapBody(cubit, selfPubkey: me)); + await cubit.discover('sp3'); + await tester.pumpAndSettle(); + + expect(find.text('You'), findsOneWidget); + // My own listing offers no "Message yourself" button. + expect(find.text('Message'), findsNothing); + await cubit.close(); + }); + testWidgets('renders discovered offers with their type', (tester) async { final cubit = OffersCubit(FakeOfferTransport([ _offer('1', 'Tomate rosa de Barbastro', OfferType.gift),