Merge branch 'spike/block2-derisking'
This commit is contained in:
commit
1750f3fa97
22 changed files with 576 additions and 16 deletions
|
|
@ -12,6 +12,7 @@ import 'services/coarse_location.dart';
|
||||||
import 'services/message_store.dart';
|
import 'services/message_store.dart';
|
||||||
import 'services/offer_outbox.dart';
|
import 'services/offer_outbox.dart';
|
||||||
import 'services/onboarding_store.dart';
|
import 'services/onboarding_store.dart';
|
||||||
|
import 'services/profile_store.dart';
|
||||||
import 'services/social_service.dart';
|
import 'services/social_service.dart';
|
||||||
import 'services/social_settings.dart';
|
import 'services/social_settings.dart';
|
||||||
import 'state/inventory_cubit.dart';
|
import 'state/inventory_cubit.dart';
|
||||||
|
|
@ -24,6 +25,7 @@ import 'ui/home_screen.dart';
|
||||||
import 'ui/intro_screen.dart';
|
import 'ui/intro_screen.dart';
|
||||||
import 'ui/inventory_list_screen.dart';
|
import 'ui/inventory_list_screen.dart';
|
||||||
import 'ui/market_screen.dart';
|
import 'ui/market_screen.dart';
|
||||||
|
import 'ui/profile_screen.dart';
|
||||||
import 'ui/settings_screen.dart';
|
import 'ui/settings_screen.dart';
|
||||||
import 'ui/theme.dart';
|
import 'ui/theme.dart';
|
||||||
import 'ui/variety_detail_screen.dart';
|
import 'ui/variety_detail_screen.dart';
|
||||||
|
|
@ -41,6 +43,7 @@ class TaneApp extends StatelessWidget {
|
||||||
this.location,
|
this.location,
|
||||||
this.outbox,
|
this.outbox,
|
||||||
this.messageStore,
|
this.messageStore,
|
||||||
|
this.profileStore,
|
||||||
this.showIntro = false,
|
this.showIntro = false,
|
||||||
this.autoBackup,
|
this.autoBackup,
|
||||||
super.key,
|
super.key,
|
||||||
|
|
@ -53,6 +56,7 @@ class TaneApp extends StatelessWidget {
|
||||||
location,
|
location,
|
||||||
outbox,
|
outbox,
|
||||||
messageStore,
|
messageStore,
|
||||||
|
profileStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
final VarietyRepository repository;
|
final VarietyRepository repository;
|
||||||
|
|
@ -72,6 +76,9 @@ class TaneApp extends StatelessWidget {
|
||||||
|
|
||||||
/// Optional persistence for chat history.
|
/// Optional persistence for chat history.
|
||||||
final MessageStore? messageStore;
|
final MessageStore? messageStore;
|
||||||
|
|
||||||
|
/// Optional local store for your own profile (name/about).
|
||||||
|
final ProfileStore? profileStore;
|
||||||
final bool showIntro;
|
final bool showIntro;
|
||||||
|
|
||||||
/// Drives silent periodic backups off the app lifecycle. Null in widget tests
|
/// Drives silent periodic backups off the app lifecycle. Null in widget tests
|
||||||
|
|
@ -88,6 +95,7 @@ class TaneApp extends StatelessWidget {
|
||||||
CoarseLocationProvider? location,
|
CoarseLocationProvider? location,
|
||||||
OfferOutbox? outbox,
|
OfferOutbox? outbox,
|
||||||
MessageStore? messageStore,
|
MessageStore? messageStore,
|
||||||
|
ProfileStore? profileStore,
|
||||||
) {
|
) {
|
||||||
return GoRouter(
|
return GoRouter(
|
||||||
initialLocation: showIntro ? '/intro' : '/',
|
initialLocation: showIntro ? '/intro' : '/',
|
||||||
|
|
@ -112,6 +120,15 @@ class TaneApp extends StatelessWidget {
|
||||||
path: '/messages',
|
path: '/messages',
|
||||||
builder: (context, state) => ChatListScreen(store: messageStore),
|
builder: (context, state) => ChatListScreen(store: messageStore),
|
||||||
),
|
),
|
||||||
|
if (social != null && socialSettings != null && profileStore != null)
|
||||||
|
GoRoute(
|
||||||
|
path: '/profile',
|
||||||
|
builder: (context, state) => ProfileScreen(
|
||||||
|
social: social,
|
||||||
|
settings: socialSettings,
|
||||||
|
profileStore: profileStore,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (social != null && socialSettings != null)
|
if (social != null && socialSettings != null)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/chat/:pubkey',
|
path: '/chat/:pubkey',
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import '../services/recovery_sheet_service.dart';
|
||||||
import '../services/share_catalog_service.dart';
|
import '../services/share_catalog_service.dart';
|
||||||
import '../services/message_store.dart';
|
import '../services/message_store.dart';
|
||||||
import '../services/offer_outbox.dart';
|
import '../services/offer_outbox.dart';
|
||||||
|
import '../services/profile_store.dart';
|
||||||
import '../services/social_service.dart';
|
import '../services/social_service.dart';
|
||||||
import '../services/social_settings.dart';
|
import '../services/social_settings.dart';
|
||||||
|
|
||||||
|
|
@ -92,6 +93,7 @@ Future<void> configureDependencies() async {
|
||||||
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
..registerSingleton<SocialSettings>(SocialSettings(secretStore))
|
||||||
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
..registerSingleton<OfferOutbox>(OfferOutbox(secretStore))
|
||||||
..registerSingleton<MessageStore>(MessageStore(secretStore))
|
..registerSingleton<MessageStore>(MessageStore(secretStore))
|
||||||
|
..registerSingleton<ProfileStore>(ProfileStore(secretStore))
|
||||||
..registerSingleton<ExportImportService>(
|
..registerSingleton<ExportImportService>(
|
||||||
ExportImportService(
|
ExportImportService(
|
||||||
repository: varietyRepository,
|
repository: varietyRepository,
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,19 @@
|
||||||
"locationFailed": "Couldn't get your location — check that location is on and the permission is granted",
|
"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"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Your profile",
|
||||||
|
"name": "Display name",
|
||||||
|
"nameHint": "How others see you",
|
||||||
|
"about": "About",
|
||||||
|
"aboutHint": "A short line — what you grow, where",
|
||||||
|
"yourId": "Your identity",
|
||||||
|
"idHelp": "Share this so people can recognise you",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"save": "Save",
|
||||||
|
"saved": "Profile saved"
|
||||||
|
},
|
||||||
"chatList": {
|
"chatList": {
|
||||||
"title": "Messages",
|
"title": "Messages",
|
||||||
"empty": "No conversations yet. Message someone from the market."
|
"empty": "No conversations yet. Message someone from the market."
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,19 @@
|
||||||
"locationFailed": "No se pudo obtener tu ubicación — comprueba que la ubicación está activada y el permiso concedido",
|
"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"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Tu perfil",
|
||||||
|
"name": "Nombre",
|
||||||
|
"nameHint": "Cómo te ven los demás",
|
||||||
|
"about": "Sobre ti",
|
||||||
|
"aboutHint": "Una línea — qué cultivas, dónde",
|
||||||
|
"yourId": "Tu identidad",
|
||||||
|
"idHelp": "Compártela para que te reconozcan",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copied": "Copiado",
|
||||||
|
"save": "Guardar",
|
||||||
|
"saved": "Perfil guardado"
|
||||||
|
},
|
||||||
"chatList": {
|
"chatList": {
|
||||||
"title": "Mensajes",
|
"title": "Mensajes",
|
||||||
"empty": "Aún no hay conversaciones. Escribe a alguien desde el mercado."
|
"empty": "Aún no hay conversaciones. Escribe a alguien desde el mercado."
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,19 @@
|
||||||
"locationFailed": "Não foi possível obter a tua localização — verifica se a localização está ativa e a permissão concedida",
|
"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"
|
||||||
},
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "O teu perfil",
|
||||||
|
"name": "Nome",
|
||||||
|
"nameHint": "Como os outros te veem",
|
||||||
|
"about": "Sobre ti",
|
||||||
|
"aboutHint": "Uma linha — o que cultivas, onde",
|
||||||
|
"yourId": "A tua identidade",
|
||||||
|
"idHelp": "Partilha-a para que te reconheçam",
|
||||||
|
"copy": "Copiar",
|
||||||
|
"copied": "Copiado",
|
||||||
|
"save": "Guardar",
|
||||||
|
"saved": "Perfil guardado"
|
||||||
|
},
|
||||||
"chatList": {
|
"chatList": {
|
||||||
"title": "Mensagens",
|
"title": "Mensagens",
|
||||||
"empty": "Ainda não há conversas. Escreve a alguém a partir do mercado."
|
"empty": "Ainda não há conversas. Escreve a alguém a partir do mercado."
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 3
|
/// Locales: 3
|
||||||
/// Strings: 998 (332 per locale)
|
/// Strings: 1031 (343 per locale)
|
||||||
///
|
///
|
||||||
/// Built on 2026-07-10 at 10:26 UTC
|
/// Built on 2026-07-10 at 10:36 UTC
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
|
||||||
late final Translations$desiccant$en desiccant = Translations$desiccant$en.internal(_root);
|
late final Translations$desiccant$en desiccant = Translations$desiccant$en.internal(_root);
|
||||||
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
late final Translations$unit$en unit = Translations$unit$en.internal(_root);
|
||||||
late final Translations$market$en market = Translations$market$en.internal(_root);
|
late final Translations$market$en market = Translations$market$en.internal(_root);
|
||||||
|
late final Translations$profile$en profile = Translations$profile$en.internal(_root);
|
||||||
late final Translations$chatList$en chatList = Translations$chatList$en.internal(_root);
|
late final Translations$chatList$en chatList = Translations$chatList$en.internal(_root);
|
||||||
late final Translations$chat$en chat = Translations$chat$en.internal(_root);
|
late final Translations$chat$en chat = Translations$chat$en.internal(_root);
|
||||||
late final Translations$trust$en trust = Translations$trust$en.internal(_root);
|
late final Translations$trust$en trust = Translations$trust$en.internal(_root);
|
||||||
|
|
@ -1143,6 +1144,48 @@ class Translations$market$en {
|
||||||
String get queued => 'Saved — we\'ll share these when you\'re connected';
|
String get queued => 'Saved — we\'ll share these when you\'re connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: profile
|
||||||
|
class Translations$profile$en {
|
||||||
|
Translations$profile$en.internal(this._root);
|
||||||
|
|
||||||
|
final Translations _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
|
||||||
|
/// en: 'Your profile'
|
||||||
|
String get title => 'Your profile';
|
||||||
|
|
||||||
|
/// en: 'Display name'
|
||||||
|
String get name => 'Display name';
|
||||||
|
|
||||||
|
/// en: 'How others see you'
|
||||||
|
String get nameHint => 'How others see you';
|
||||||
|
|
||||||
|
/// en: 'About'
|
||||||
|
String get about => 'About';
|
||||||
|
|
||||||
|
/// en: 'A short line — what you grow, where'
|
||||||
|
String get aboutHint => 'A short line — what you grow, where';
|
||||||
|
|
||||||
|
/// en: 'Your identity'
|
||||||
|
String get yourId => 'Your identity';
|
||||||
|
|
||||||
|
/// en: 'Share this so people can recognise you'
|
||||||
|
String get idHelp => 'Share this so people can recognise you';
|
||||||
|
|
||||||
|
/// en: 'Copy'
|
||||||
|
String get copy => 'Copy';
|
||||||
|
|
||||||
|
/// en: 'Copied'
|
||||||
|
String get copied => 'Copied';
|
||||||
|
|
||||||
|
/// en: 'Save'
|
||||||
|
String get save => 'Save';
|
||||||
|
|
||||||
|
/// en: 'Profile saved'
|
||||||
|
String get saved => 'Profile saved';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: chatList
|
// Path: chatList
|
||||||
class Translations$chatList$en {
|
class Translations$chatList$en {
|
||||||
Translations$chatList$en.internal(this._root);
|
Translations$chatList$en.internal(this._root);
|
||||||
|
|
@ -1985,6 +2028,17 @@ extension on Translations {
|
||||||
'market.useLocation' => 'Use my approximate location',
|
'market.useLocation' => 'Use my approximate location',
|
||||||
'market.locationFailed' => 'Couldn\'t get your location — check that location is on and the permission is granted',
|
'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.queued' => 'Saved — we\'ll share these when you\'re connected',
|
||||||
|
'profile.title' => 'Your profile',
|
||||||
|
'profile.name' => 'Display name',
|
||||||
|
'profile.nameHint' => 'How others see you',
|
||||||
|
'profile.about' => 'About',
|
||||||
|
'profile.aboutHint' => 'A short line — what you grow, where',
|
||||||
|
'profile.yourId' => 'Your identity',
|
||||||
|
'profile.idHelp' => 'Share this so people can recognise you',
|
||||||
|
'profile.copy' => 'Copy',
|
||||||
|
'profile.copied' => 'Copied',
|
||||||
|
'profile.save' => 'Save',
|
||||||
|
'profile.saved' => 'Profile saved',
|
||||||
'chatList.title' => 'Messages',
|
'chatList.title' => 'Messages',
|
||||||
'chatList.empty' => 'No conversations yet. Message someone from the market.',
|
'chatList.empty' => 'No conversations yet. Message someone from the market.',
|
||||||
'chat.title' => 'Chat',
|
'chat.title' => 'Chat',
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$desiccant$es desiccant = _Translations$desiccant$es._(_root);
|
@override late final _Translations$desiccant$es desiccant = _Translations$desiccant$es._(_root);
|
||||||
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
@override late final _Translations$unit$es unit = _Translations$unit$es._(_root);
|
||||||
@override late final _Translations$market$es market = _Translations$market$es._(_root);
|
@override late final _Translations$market$es market = _Translations$market$es._(_root);
|
||||||
|
@override late final _Translations$profile$es profile = _Translations$profile$es._(_root);
|
||||||
@override late final _Translations$chatList$es chatList = _Translations$chatList$es._(_root);
|
@override late final _Translations$chatList$es chatList = _Translations$chatList$es._(_root);
|
||||||
@override late final _Translations$chat$es chat = _Translations$chat$es._(_root);
|
@override late final _Translations$chat$es chat = _Translations$chat$es._(_root);
|
||||||
@override late final _Translations$trust$es trust = _Translations$trust$es._(_root);
|
@override late final _Translations$trust$es trust = _Translations$trust$es._(_root);
|
||||||
|
|
@ -635,6 +636,26 @@ class _Translations$market$es extends Translations$market$en {
|
||||||
@override String get queued => 'Guardado — las compartiremos cuando tengas conexión';
|
@override String get queued => 'Guardado — las compartiremos cuando tengas conexión';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: profile
|
||||||
|
class _Translations$profile$es extends Translations$profile$en {
|
||||||
|
_Translations$profile$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsEs _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'Tu perfil';
|
||||||
|
@override String get name => 'Nombre';
|
||||||
|
@override String get nameHint => 'Cómo te ven los demás';
|
||||||
|
@override String get about => 'Sobre ti';
|
||||||
|
@override String get aboutHint => 'Una línea — qué cultivas, dónde';
|
||||||
|
@override String get yourId => 'Tu identidad';
|
||||||
|
@override String get idHelp => 'Compártela para que te reconozcan';
|
||||||
|
@override String get copy => 'Copiar';
|
||||||
|
@override String get copied => 'Copiado';
|
||||||
|
@override String get save => 'Guardar';
|
||||||
|
@override String get saved => 'Perfil guardado';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: chatList
|
// Path: chatList
|
||||||
class _Translations$chatList$es extends Translations$chatList$en {
|
class _Translations$chatList$es extends Translations$chatList$en {
|
||||||
_Translations$chatList$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
_Translations$chatList$es._(TranslationsEs root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -1337,6 +1358,17 @@ extension on TranslationsEs {
|
||||||
'market.useLocation' => 'Usar mi ubicación aproximada',
|
'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.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.queued' => 'Guardado — las compartiremos cuando tengas conexión',
|
||||||
|
'profile.title' => 'Tu perfil',
|
||||||
|
'profile.name' => 'Nombre',
|
||||||
|
'profile.nameHint' => 'Cómo te ven los demás',
|
||||||
|
'profile.about' => 'Sobre ti',
|
||||||
|
'profile.aboutHint' => 'Una línea — qué cultivas, dónde',
|
||||||
|
'profile.yourId' => 'Tu identidad',
|
||||||
|
'profile.idHelp' => 'Compártela para que te reconozcan',
|
||||||
|
'profile.copy' => 'Copiar',
|
||||||
|
'profile.copied' => 'Copiado',
|
||||||
|
'profile.save' => 'Guardar',
|
||||||
|
'profile.saved' => 'Perfil guardado',
|
||||||
'chatList.title' => 'Mensajes',
|
'chatList.title' => 'Mensajes',
|
||||||
'chatList.empty' => 'Aún no hay conversaciones. Escribe a alguien desde el mercado.',
|
'chatList.empty' => 'Aún no hay conversaciones. Escribe a alguien desde el mercado.',
|
||||||
'chat.title' => 'Chat',
|
'chat.title' => 'Chat',
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ class TranslationsPt extends Translations with BaseTranslations<AppLocale, Trans
|
||||||
@override late final _Translations$desiccant$pt desiccant = _Translations$desiccant$pt._(_root);
|
@override late final _Translations$desiccant$pt desiccant = _Translations$desiccant$pt._(_root);
|
||||||
@override late final _Translations$unit$pt unit = _Translations$unit$pt._(_root);
|
@override late final _Translations$unit$pt unit = _Translations$unit$pt._(_root);
|
||||||
@override late final _Translations$market$pt market = _Translations$market$pt._(_root);
|
@override late final _Translations$market$pt market = _Translations$market$pt._(_root);
|
||||||
|
@override late final _Translations$profile$pt profile = _Translations$profile$pt._(_root);
|
||||||
@override late final _Translations$chatList$pt chatList = _Translations$chatList$pt._(_root);
|
@override late final _Translations$chatList$pt chatList = _Translations$chatList$pt._(_root);
|
||||||
@override late final _Translations$chat$pt chat = _Translations$chat$pt._(_root);
|
@override late final _Translations$chat$pt chat = _Translations$chat$pt._(_root);
|
||||||
@override late final _Translations$trust$pt trust = _Translations$trust$pt._(_root);
|
@override late final _Translations$trust$pt trust = _Translations$trust$pt._(_root);
|
||||||
|
|
@ -631,6 +632,26 @@ class _Translations$market$pt extends Translations$market$en {
|
||||||
@override String get queued => 'Guardado — vamos partilhá-las quando tiveres ligação';
|
@override String get queued => 'Guardado — vamos partilhá-las quando tiveres ligação';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path: profile
|
||||||
|
class _Translations$profile$pt extends Translations$profile$en {
|
||||||
|
_Translations$profile$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||||
|
|
||||||
|
final TranslationsPt _root; // ignore: unused_field
|
||||||
|
|
||||||
|
// Translations
|
||||||
|
@override String get title => 'O teu perfil';
|
||||||
|
@override String get name => 'Nome';
|
||||||
|
@override String get nameHint => 'Como os outros te veem';
|
||||||
|
@override String get about => 'Sobre ti';
|
||||||
|
@override String get aboutHint => 'Uma linha — o que cultivas, onde';
|
||||||
|
@override String get yourId => 'A tua identidade';
|
||||||
|
@override String get idHelp => 'Partilha-a para que te reconheçam';
|
||||||
|
@override String get copy => 'Copiar';
|
||||||
|
@override String get copied => 'Copiado';
|
||||||
|
@override String get save => 'Guardar';
|
||||||
|
@override String get saved => 'Perfil guardado';
|
||||||
|
}
|
||||||
|
|
||||||
// Path: chatList
|
// Path: chatList
|
||||||
class _Translations$chatList$pt extends Translations$chatList$en {
|
class _Translations$chatList$pt extends Translations$chatList$en {
|
||||||
_Translations$chatList$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
_Translations$chatList$pt._(TranslationsPt root) : this._root = root, super.internal(root);
|
||||||
|
|
@ -1329,6 +1350,17 @@ extension on TranslationsPt {
|
||||||
'market.useLocation' => 'Usar a minha localização aproximada',
|
'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.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.queued' => 'Guardado — vamos partilhá-las quando tiveres ligação',
|
||||||
|
'profile.title' => 'O teu perfil',
|
||||||
|
'profile.name' => 'Nome',
|
||||||
|
'profile.nameHint' => 'Como os outros te veem',
|
||||||
|
'profile.about' => 'Sobre ti',
|
||||||
|
'profile.aboutHint' => 'Uma linha — o que cultivas, onde',
|
||||||
|
'profile.yourId' => 'A tua identidade',
|
||||||
|
'profile.idHelp' => 'Partilha-a para que te reconheçam',
|
||||||
|
'profile.copy' => 'Copiar',
|
||||||
|
'profile.copied' => 'Copiado',
|
||||||
|
'profile.save' => 'Guardar',
|
||||||
|
'profile.saved' => 'Perfil guardado',
|
||||||
'chatList.title' => 'Mensagens',
|
'chatList.title' => 'Mensagens',
|
||||||
'chatList.empty' => 'Ainda não há conversas. Escreve a alguém a partir do mercado.',
|
'chatList.empty' => 'Ainda não há conversas. Escreve a alguém a partir do mercado.',
|
||||||
'chat.title' => 'Conversa',
|
'chat.title' => 'Conversa',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import 'services/coarse_location.dart';
|
||||||
import 'services/message_store.dart';
|
import 'services/message_store.dart';
|
||||||
import 'services/offer_outbox.dart';
|
import 'services/offer_outbox.dart';
|
||||||
import 'services/onboarding_store.dart';
|
import 'services/onboarding_store.dart';
|
||||||
|
import 'services/profile_store.dart';
|
||||||
import 'services/social_service.dart';
|
import 'services/social_service.dart';
|
||||||
import 'services/social_settings.dart';
|
import 'services/social_settings.dart';
|
||||||
|
|
||||||
|
|
@ -29,6 +30,7 @@ Future<void> main() async {
|
||||||
location: const GeolocatorCoarseLocation(),
|
location: const GeolocatorCoarseLocation(),
|
||||||
outbox: getIt<OfferOutbox>(),
|
outbox: getIt<OfferOutbox>(),
|
||||||
messageStore: getIt<MessageStore>(),
|
messageStore: getIt<MessageStore>(),
|
||||||
|
profileStore: getIt<ProfileStore>(),
|
||||||
showIntro: !await onboarding.introSeen(),
|
showIntro: !await onboarding.introSeen(),
|
||||||
autoBackup: getIt.isRegistered<AutoBackupService>()
|
autoBackup: getIt.isRegistered<AutoBackupService>()
|
||||||
? getIt<AutoBackupService>()
|
? getIt<AutoBackupService>()
|
||||||
|
|
|
||||||
20
apps/app_seeds/lib/services/profile_store.dart
Normal file
20
apps/app_seeds/lib/services/profile_store.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import '../security/secret_store.dart';
|
||||||
|
|
||||||
|
/// Your own display name and short "about", persisted locally (keystore-backed,
|
||||||
|
/// no plaintext at rest). The network copy is a published NIP-01 kind:0 event;
|
||||||
|
/// this local copy prefills the editor and works offline.
|
||||||
|
class ProfileStore {
|
||||||
|
ProfileStore(this._store);
|
||||||
|
|
||||||
|
final SecretStore _store;
|
||||||
|
static const _nameKey = 'tane.social.profile.name';
|
||||||
|
static const _aboutKey = 'tane.social.profile.about';
|
||||||
|
|
||||||
|
Future<String> name() async => (await _store.read(_nameKey)) ?? '';
|
||||||
|
Future<String> about() async => (await _store.read(_aboutKey)) ?? '';
|
||||||
|
|
||||||
|
Future<void> save({required String name, required String about}) async {
|
||||||
|
await _store.write(_nameKey, name.trim());
|
||||||
|
await _store.write(_aboutKey, about.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,13 +51,15 @@ class SocialSession {
|
||||||
SocialSession(this._channel)
|
SocialSession(this._channel)
|
||||||
: offers = NostrOfferTransport(_channel),
|
: offers = NostrOfferTransport(_channel),
|
||||||
messages = NostrMessageTransport(_channel),
|
messages = NostrMessageTransport(_channel),
|
||||||
trust = NostrTrustTransport(_channel);
|
trust = NostrTrustTransport(_channel),
|
||||||
|
profile = NostrProfileTransport(_channel);
|
||||||
|
|
||||||
final NostrChannel _channel;
|
final NostrChannel _channel;
|
||||||
|
|
||||||
final OfferTransport offers;
|
final OfferTransport offers;
|
||||||
final MessageTransport messages;
|
final MessageTransport messages;
|
||||||
final TrustTransport trust;
|
final TrustTransport trust;
|
||||||
|
final ProfileTransport profile;
|
||||||
|
|
||||||
Future<void> close() => _channel.close();
|
Future<void> close() => _channel.close();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,27 @@ class AppDrawer extends StatelessWidget {
|
||||||
onTap: marketEnabled
|
onTap: marketEnabled
|
||||||
? () {
|
? () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
context.go('/market');
|
context.push('/market');
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
_DrawerItem(
|
||||||
|
icon: const Icon(Icons.person),
|
||||||
|
label: t.menu.profile,
|
||||||
|
onTap: marketEnabled
|
||||||
|
? () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.push('/profile');
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
_DrawerItem(icon: const Icon(Icons.person), label: t.menu.profile),
|
|
||||||
_DrawerItem(
|
_DrawerItem(
|
||||||
icon: const Icon(Icons.chat_bubble),
|
icon: const Icon(Icons.chat_bubble),
|
||||||
label: t.menu.chat,
|
label: t.menu.chat,
|
||||||
onTap: marketEnabled
|
onTap: marketEnabled
|
||||||
? () {
|
? () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
context.go('/messages');
|
context.push('/messages');
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class _ChatListScreenState extends State<ChatListScreen> {
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
onTap: () => context.go('/chat/${c.peerPubkey}'),
|
onTap: () => context.push('/chat/${c.peerPubkey}'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
subtitle: t.home.openMarketSubtitle,
|
subtitle: t.home.openMarketSubtitle,
|
||||||
tag: marketEnabled ? null : t.common.comingSoon,
|
tag: marketEnabled ? null : t.common.comingSoon,
|
||||||
onTap: marketEnabled
|
onTap: marketEnabled
|
||||||
? () => context.go('/market')
|
? () => context.push('/market')
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -148,13 +148,6 @@ class _MarketScreenState extends State<MarketScreen> {
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(t.market.title),
|
title: Text(t.market.title),
|
||||||
actions: [
|
actions: [
|
||||||
if (cubit != null && (cubit.isOnline || widget.outbox != null))
|
|
||||||
IconButton(
|
|
||||||
key: const Key('market.shareMine'),
|
|
||||||
icon: const Icon(Icons.ios_share_outlined),
|
|
||||||
tooltip: t.market.shareMine,
|
|
||||||
onPressed: _shareMine,
|
|
||||||
),
|
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const Key('market.config'),
|
key: const Key('market.config'),
|
||||||
icon: const Icon(Icons.tune),
|
icon: const Icon(Icons.tune),
|
||||||
|
|
@ -163,6 +156,15 @@ class _MarketScreenState extends State<MarketScreen> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
floatingActionButton:
|
||||||
|
cubit != null && (cubit.isOnline || widget.outbox != null)
|
||||||
|
? FloatingActionButton.extended(
|
||||||
|
key: const Key('market.shareMine'),
|
||||||
|
onPressed: _shareMine,
|
||||||
|
icon: const Icon(Icons.campaign_outlined),
|
||||||
|
label: Text(t.market.shareMine),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
body: _loading || cubit == null
|
body: _loading || cubit == null
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: BlocProvider.value(
|
: BlocProvider.value(
|
||||||
|
|
@ -232,7 +234,7 @@ class MarketBody extends StatelessWidget {
|
||||||
return _OfferCard(
|
return _OfferCard(
|
||||||
offer: o,
|
offer: o,
|
||||||
mine: mine,
|
mine: mine,
|
||||||
onContact: mine ? null : () => context.go('/chat/${o.authorPubkeyHex}'),
|
onContact: mine ? null : () => context.push('/chat/${o.authorPubkeyHex}'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
190
apps/app_seeds/lib/ui/profile_screen.dart
Normal file
190
apps/app_seeds/lib/ui/profile_screen.dart
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../i18n/strings.g.dart';
|
||||||
|
import '../services/profile_store.dart';
|
||||||
|
import '../services/social_service.dart';
|
||||||
|
import '../services/social_settings.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
|
/// Your own profile: the shareable identity (npub) plus a display name and short
|
||||||
|
/// "about" others see instead of a raw key. Saving persists locally and, when
|
||||||
|
/// online, publishes NIP-01 kind:0 metadata so peers can recognise you.
|
||||||
|
class ProfileScreen extends StatefulWidget {
|
||||||
|
const ProfileScreen({
|
||||||
|
required this.social,
|
||||||
|
required this.settings,
|
||||||
|
required this.profileStore,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SocialService social;
|
||||||
|
final SocialSettings settings;
|
||||||
|
final ProfileStore profileStore;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProfileScreen> createState() => _ProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfileScreenState extends State<ProfileScreen> {
|
||||||
|
final _name = TextEditingController();
|
||||||
|
final _about = TextEditingController();
|
||||||
|
bool _loading = true;
|
||||||
|
bool _saving = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
_name.text = await widget.profileStore.name();
|
||||||
|
_about.text = await widget.profileStore.about();
|
||||||
|
if (mounted) setState(() => _loading = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyId() async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final t = context.t;
|
||||||
|
await Clipboard.setData(ClipboardData(text: widget.social.npub));
|
||||||
|
if (!mounted) return;
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(t.profile.copied)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final t = context.t;
|
||||||
|
setState(() => _saving = true);
|
||||||
|
await widget.profileStore.save(name: _name.text, about: _about.text);
|
||||||
|
|
||||||
|
// Best-effort publish to the network so peers see the name.
|
||||||
|
final relays = await widget.settings.relayUrls();
|
||||||
|
if (relays.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final session = await widget.social.openSession(relays);
|
||||||
|
await session.profile.publish(
|
||||||
|
name: _name.text.trim(),
|
||||||
|
about: _about.text.trim(),
|
||||||
|
);
|
||||||
|
await session.close();
|
||||||
|
} catch (_) {
|
||||||
|
// offline / unreachable — the local copy is saved regardless.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _saving = false);
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(t.profile.saved)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_name.dispose();
|
||||||
|
_about.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(t.profile.title)),
|
||||||
|
body: _loading
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: ListView(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
children: [
|
||||||
|
_IdentityCard(npub: widget.social.npub, onCopy: _copyId),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
TextField(
|
||||||
|
key: const Key('profile.name'),
|
||||||
|
controller: _name,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.profile.name,
|
||||||
|
helperText: t.profile.nameHint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
key: const Key('profile.about'),
|
||||||
|
controller: _about,
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 4,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: t.profile.about,
|
||||||
|
helperText: t.profile.aboutHint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
FilledButton(
|
||||||
|
key: const Key('profile.save'),
|
||||||
|
onPressed: _saving ? null : _save,
|
||||||
|
child: _saving
|
||||||
|
? const SizedBox(
|
||||||
|
height: 18,
|
||||||
|
width: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Text(t.profile.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IdentityCard extends StatelessWidget {
|
||||||
|
const _IdentityCard({required this.npub, required this.onCopy});
|
||||||
|
|
||||||
|
final String npub;
|
||||||
|
final VoidCallback onCopy;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.t;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: seedPrimaryContainer.withValues(alpha: 0.4),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: seedOutline),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
t.profile.yourId,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: seedOnSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
t.profile.idHelp,
|
||||||
|
style: const TextStyle(color: seedMuted, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
SelectableText(
|
||||||
|
npub,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
color: seedOnSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: TextButton.icon(
|
||||||
|
key: const Key('profile.copyId'),
|
||||||
|
onPressed: onCopy,
|
||||||
|
icon: const Icon(Icons.copy, size: 16),
|
||||||
|
label: Text(t.profile.copy),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/app_seeds/test/services/profile_store_test.dart
Normal file
20
apps/app_seeds/test/services/profile_store_test.dart
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:tane/services/profile_store.dart';
|
||||||
|
|
||||||
|
import '../support/test_support.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late ProfileStore store;
|
||||||
|
setUp(() => store = ProfileStore(InMemorySecretStore()));
|
||||||
|
|
||||||
|
test('defaults are empty', () async {
|
||||||
|
expect(await store.name(), '');
|
||||||
|
expect(await store.about(), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save then read back, trimmed', () async {
|
||||||
|
await store.save(name: ' Alicia ', about: ' tomate rosa ');
|
||||||
|
expect(await store.name(), 'Alicia');
|
||||||
|
expect(await store.about(), 'tomate rosa');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -18,10 +18,12 @@ export 'src/social/nostr/nostr_channel.dart';
|
||||||
export 'src/social/nostr/nostr_connection.dart';
|
export 'src/social/nostr/nostr_connection.dart';
|
||||||
export 'src/social/nostr/nostr_message_transport.dart';
|
export 'src/social/nostr/nostr_message_transport.dart';
|
||||||
export 'src/social/nostr/nostr_offer_transport.dart';
|
export 'src/social/nostr/nostr_offer_transport.dart';
|
||||||
|
export 'src/social/nostr/nostr_profile_transport.dart';
|
||||||
export 'src/social/nostr/nostr_trust_transport.dart';
|
export 'src/social/nostr/nostr_trust_transport.dart';
|
||||||
export 'src/social/nostr/relay_pool.dart';
|
export 'src/social/nostr/relay_pool.dart';
|
||||||
export 'src/social/offer.dart';
|
export 'src/social/offer.dart';
|
||||||
export 'src/social/offer_transport.dart';
|
export 'src/social/offer_transport.dart';
|
||||||
|
export 'src/social/profile_transport.dart';
|
||||||
export 'src/social/trust_transport.dart';
|
export 'src/social/trust_transport.dart';
|
||||||
export 'src/social/web_of_trust.dart';
|
export 'src/social/web_of_trust.dart';
|
||||||
export 'src/value/quantity.dart';
|
export 'src/value/quantity.dart';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:nostr/nostr.dart';
|
||||||
|
|
||||||
|
import '../profile_transport.dart';
|
||||||
|
import 'nostr_channel.dart';
|
||||||
|
|
||||||
|
/// Nostr NIP-01 kind:0 backend for [ProfileTransport], on the shared channel.
|
||||||
|
class NostrProfileTransport implements ProfileTransport {
|
||||||
|
NostrProfileTransport(this._conn);
|
||||||
|
|
||||||
|
final NostrChannel _conn;
|
||||||
|
|
||||||
|
/// NIP-01 set_metadata (replaceable).
|
||||||
|
static const kindMetadata = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> publish({
|
||||||
|
String name = '',
|
||||||
|
String about = '',
|
||||||
|
String picture = '',
|
||||||
|
}) async {
|
||||||
|
final content = jsonEncode({
|
||||||
|
if (name.isNotEmpty) 'name': name,
|
||||||
|
if (about.isNotEmpty) 'about': about,
|
||||||
|
if (picture.isNotEmpty) 'picture': picture,
|
||||||
|
});
|
||||||
|
final event = Event.from(
|
||||||
|
kind: kindMetadata,
|
||||||
|
content: content,
|
||||||
|
tags: const [],
|
||||||
|
secretKey: _conn.privateKeyHex,
|
||||||
|
);
|
||||||
|
await _conn.publish(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserProfile?> fetch(String pubkeyHex) async {
|
||||||
|
final events = await _conn.reqOnce(
|
||||||
|
Filter(kinds: const [kindMetadata], authors: [pubkeyHex], limit: 1),
|
||||||
|
);
|
||||||
|
if (events.isEmpty) return null;
|
||||||
|
// Newest wins if several slipped through.
|
||||||
|
events.sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
|
final event = events.first;
|
||||||
|
try {
|
||||||
|
final map = jsonDecode(event.content) as Map<String, dynamic>;
|
||||||
|
return UserProfile(
|
||||||
|
pubkeyHex: pubkeyHex,
|
||||||
|
name: (map['name'] as String?) ?? '',
|
||||||
|
about: (map['about'] as String?) ?? '',
|
||||||
|
picture: (map['picture'] as String?) ?? '',
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return UserProfile(pubkeyHex: pubkeyHex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() => _conn.close();
|
||||||
|
}
|
||||||
30
packages/commons_core/lib/src/social/profile_transport.dart
Normal file
30
packages/commons_core/lib/src/social/profile_transport.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
/// A user's public profile (NIP-01 kind:0 metadata) — the human name others see
|
||||||
|
/// instead of a raw key. Only what the person chooses to publish.
|
||||||
|
class UserProfile {
|
||||||
|
const UserProfile({
|
||||||
|
required this.pubkeyHex,
|
||||||
|
this.name = '',
|
||||||
|
this.about = '',
|
||||||
|
this.picture = '',
|
||||||
|
});
|
||||||
|
|
||||||
|
final String pubkeyHex;
|
||||||
|
final String name;
|
||||||
|
final String about;
|
||||||
|
final String picture;
|
||||||
|
|
||||||
|
bool get isEmpty => name.isEmpty && about.isEmpty && picture.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish/fetch public profile metadata. Kind:0 is replaceable — publishing
|
||||||
|
/// again overwrites. Part of the social layer (identity is generic core), a
|
||||||
|
/// sibling of the offer/message/trust transports over the same connection.
|
||||||
|
abstract interface class ProfileTransport {
|
||||||
|
/// Publishes (replaces) this identity's profile.
|
||||||
|
Future<void> publish({String name, String about, String picture});
|
||||||
|
|
||||||
|
/// Fetches [pubkeyHex]'s latest profile, or null if they never published one.
|
||||||
|
Future<UserProfile?> fetch(String pubkeyHex);
|
||||||
|
|
||||||
|
Future<void> close();
|
||||||
|
}
|
||||||
46
packages/commons_core/test/social/nostr_profile_test.dart
Normal file
46
packages/commons_core/test/social/nostr_profile_test.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../support/mini_relay.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Future<NostrIdentity> idFor(int fill) =>
|
||||||
|
NostrKeyDerivation.deriveFromSeed(Uint8List(32)..fillRange(0, 32, fill));
|
||||||
|
|
||||||
|
late MiniRelay relay;
|
||||||
|
setUp(() async => relay = await MiniRelay.start());
|
||||||
|
tearDown(() async => relay.stop());
|
||||||
|
|
||||||
|
test('publish then fetch round-trips a profile (kind:0)', () async {
|
||||||
|
final alice = await idFor(1);
|
||||||
|
final aliceConn = await NostrConnection.connect(relay.url, identity: alice);
|
||||||
|
await NostrProfileTransport(aliceConn)
|
||||||
|
.publish(name: 'Alicia', about: 'Guardo tomate rosa');
|
||||||
|
|
||||||
|
// Anyone else can fetch it.
|
||||||
|
final bob = await idFor(2);
|
||||||
|
final bobConn = await NostrConnection.connect(relay.url, identity: bob);
|
||||||
|
final fetched =
|
||||||
|
await NostrProfileTransport(bobConn).fetch(alice.publicKeyHex);
|
||||||
|
|
||||||
|
expect(fetched, isNotNull);
|
||||||
|
expect(fetched!.name, 'Alicia');
|
||||||
|
expect(fetched.about, 'Guardo tomate rosa');
|
||||||
|
|
||||||
|
await aliceConn.close();
|
||||||
|
await bobConn.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fetch returns null when the peer never published a profile', () async {
|
||||||
|
final me = await idFor(3);
|
||||||
|
final conn = await NostrConnection.connect(relay.url, identity: me);
|
||||||
|
final stranger = await idFor(9);
|
||||||
|
expect(
|
||||||
|
await NostrProfileTransport(conn).fetch(stranger.publicKeyHex),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
|
await conn.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue