From b51507200d6f519e65df5bc5e404f878d9ea4cd3 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Thu, 2 Jan 2025 21:35:40 -0300 Subject: [PATCH] Zap UX improvements --- lib/models/user.dart | 46 +++++---- lib/screens/app_detail_screen.dart | 2 +- lib/screens/settings_screen.dart | 145 ++++++++++++++++------------- lib/utils/nwc.dart | 22 ++--- lib/widgets/sign_in_container.dart | 133 ++++++++++++++------------ lib/widgets/users_rich_text.dart | 53 +++++++++++ lib/widgets/wot_container.dart | 52 +---------- lib/widgets/zap_button.dart | 102 ++++++++------------ lib/widgets/zap_receipts.dart | 33 +------ pubspec.lock | 4 +- pubspec.yaml | 2 +- 11 files changed, 286 insertions(+), 308 deletions(-) create mode 100644 lib/widgets/users_rich_text.dart diff --git a/lib/models/user.dart b/lib/models/user.dart index 15dc2f4..6266cf5 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -22,13 +22,17 @@ class User extends base.User with DataModelMixin { {required base.Event event, String? comment}) async { final adapter = DataModel.adapterFor(this) as NostrAdapter; + // First ensure connection is available + final nwcConnection = adapter.ref.read(nwcConnectionProvider).asData?.value; + if (nwcConnection == null) { + throw 'No NWC connection'; + } + final authorPubkey = event.event.pubkey; - print( - 'bro wants to zap $amountInSats to $authorPubkey for ${event.event.id}'); final author = adapter.ref.users.findOneLocalById(authorPubkey); if (author?.lud16 == null) { - throw 'cant zap'; + throw 'Recipient has no Lightning address'; } final amountInMillisats = amountInSats * 1000; @@ -44,27 +48,27 @@ class User extends base.User with DataModelMixin { await partialZapRequest.signWith(amberSigner, withPubkey: pubkey); // Now we fetch the invoice - final lnurlResponse = await author.fetchLnUrl(); + final lnResponse = await author.fetchLightningAddress(); - final recipientAllowsNostr = lnurlResponse['allowsNostr']; - final recipientPubkey = lnurlResponse['nostrPubkey']; + final recipientAllowsNostr = lnResponse['allowsNostr']; + final recipientPubkey = lnResponse['nostrPubkey']; // All amounts are in millisats - final recipientMin = lnurlResponse['minSendable']; - final recipientMax = lnurlResponse['maxSendable']; + final recipientMin = lnResponse['minSendable']; + final recipientMax = lnResponse['maxSendable']; if (recipientAllowsNostr != true && recipientPubkey != null) { - throw 'cant zap, recipient does not allow nostr'; + throw 'Recipient does not allow nostr'; } if ((recipientMin != null && amountInMillisats < recipientMin) || (recipientMax != null && amountInMillisats > recipientMax)) { - throw 'cant zap, amount not between min and max sendable'; + throw 'Amount not between min and max sendable'; } - final commentLength = lnurlResponse['commentAllowed']; + final commentLength = lnResponse['commentAllowed']; comment = commentLength != null ? comment?.substringMax(commentLength) : comment; - var callbackUri = Uri.parse(lnurlResponse['callback']!); + var callbackUri = Uri.parse(lnResponse['callback']!); callbackUri = callbackUri.replace( queryParameters: { ...callbackUri.queryParameters, @@ -74,26 +78,20 @@ class User extends base.User with DataModelMixin { }, ); - final response = await http.get(callbackUri); - final invoiceMap = jsonDecode(response.body); + final invoiceResponse = await http.get(callbackUri); + final invoiceMap = jsonDecode(invoiceResponse.body); final invoice = invoiceMap['pr']; if (invoice == null) { - throw 'cant zap, no invoice'; + throw 'No invoice'; } - final nwcConnection = adapter.ref.read(nwcConnectionProvider).asData?.value; - if (nwcConnection == null) { - throw 'cant zap, no nwc connection'; - } - - // Pay the invoice via NWC - final rw = await adapter.socialRelays.ndk!.nwc.payInvoice(nwcConnection, + // Pay the invoice via NWC (errors are thrown) + await adapter.socialRelays.ndk!.nwc.payInvoice(nwcConnection, invoice: invoice, timeout: Duration(seconds: 20)); - print(rw); } - Future> fetchLnUrl() async { + Future> fetchLightningAddress() async { final [userName, domainName] = lud16!.split('@'); final lnurl = 'https://$domainName/.well-known/lnurlp/$userName'; final response = await http.get(Uri.parse(lnurl)); diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index bc4578d..978d0fb 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -16,8 +16,8 @@ import 'package:zapstore/widgets/install_button.dart'; import 'package:zapstore/widgets/release_card.dart'; import 'package:zapstore/widgets/signer_and_developer_row.dart'; import 'package:zapstore/widgets/spinning_logo.dart'; +import 'package:zapstore/widgets/users_rich_text.dart'; import 'package:zapstore/widgets/versioned_app_header.dart'; -import 'package:zapstore/widgets/wot_container.dart'; import 'package:zapstore/widgets/zap_button.dart'; import 'package:zapstore/widgets/zap_receipts.dart'; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 9577e54..1c25356 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -10,6 +10,7 @@ import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart' as http; +import 'package:ndk/domain_layer/usecases/nwc/nwc_connection.dart'; import 'package:path_provider/path_provider.dart'; import 'package:purplebase/purplebase.dart'; import 'package:zapstore/main.data.dart'; @@ -29,15 +30,11 @@ class SettingsScreen extends HookConsumerWidget { final systemInfoState = ref.watch(systemInfoNotifierProvider); final feedbackController = useTextEditingController(); - final nwcController = useTextEditingController(); final user = ref.settings .watchOne('_', alsoWatch: (_) => {_.user}) .model! .user .value; - final isTextFieldEmpty = useState(true); - - final nwcConnection = ref.watch(nwcConnectionProvider); return SingleChildScrollView( child: Column( @@ -116,67 +113,7 @@ class SettingsScreen extends HookConsumerWidget { Gap(40), Divider(), Gap(40), - if (user != null) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Gap(20), - Text( - "Nostr Wallet Connect ${nwcConnection.value != null ? 'connected' : 'URI'}"), - if (nwcConnection.value != null) - ElevatedButton( - onPressed: () async { - await ref - .read(nwcConnectionProvider.notifier) - .disconnectWallet(); - }, - style: ElevatedButton.styleFrom( - disabledBackgroundColor: Colors.transparent, - backgroundColor: Colors.transparent), - child: Text('Disconnect'), - ), - if (nwcConnection.value == null) - TextField( - autocorrect: false, - controller: nwcController, - onChanged: (value) { - isTextFieldEmpty.value = value.isEmpty; - }, - decoration: InputDecoration( - hintText: 'nostr+walletconnect://...', - suffixIcon: AsyncButtonBuilder( - disabled: isTextFieldEmpty.value, - loadingWidget: SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(), - ), - onPressed: () async { - final nwcUri = nwcController.text.trim(); - await ref - .read(nwcConnectionProvider.notifier) - .connectWallet(nwcUri); - }, - builder: (context, child, callback, buttonState) { - return Padding( - padding: const EdgeInsets.only(bottom: 2), - child: SizedBox( - child: ElevatedButton( - onPressed: callback, - style: ElevatedButton.styleFrom( - disabledBackgroundColor: Colors.transparent, - backgroundColor: Colors.transparent), - child: child, - ), - ), - ); - }, - child: Text('Connect'), - ), - ), - ), - ], - ), + if (user != null) NostrWalletConnectContainer(), Gap(40), Divider(), Gap(40), @@ -280,3 +217,81 @@ class SettingsScreen extends HookConsumerWidget { ); } } + +class NostrWalletConnectContainer extends HookConsumerWidget { + final bool dialogMode; + const NostrWalletConnectContainer({ + super.key, + this.dialogMode = false, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final nwcConnection = ref.watch(nwcConnectionProvider); + final controller = useTextEditingController(text: ''); + final isTextFieldEmpty = useState(true); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Gap(20), + Text( + "Nostr Wallet Connect ${nwcConnection.value != null ? 'connected' : 'URI'}"), + if (nwcConnection.value != null && !dialogMode) + ElevatedButton( + onPressed: () async { + await ref.read(nwcConnectionProvider.notifier).disconnectWallet(); + }, + style: ElevatedButton.styleFrom( + disabledBackgroundColor: Colors.transparent, + backgroundColor: Colors.transparent), + child: Text('Disconnect'), + ), + if (nwcConnection.value == null) + TextField( + autocorrect: false, + controller: controller, + onChanged: (value) { + isTextFieldEmpty.value = value.isEmpty; + }, + decoration: InputDecoration( + hintText: 'nostr+walletconnect://...', + suffixIcon: AsyncButtonBuilder( + disabled: isTextFieldEmpty.value, + loadingWidget: SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(), + ), + onPressed: () async { + final nwcUri = controller.text.trim(); + await ref + .read(nwcConnectionProvider.notifier) + .connectWallet(nwcUri); + if (dialogMode && context.mounted) { + Navigator.of(context).pop(); + } + }, + builder: (context, child, callback, buttonState) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: SizedBox( + child: ElevatedButton( + onPressed: callback, + style: ElevatedButton.styleFrom( + disabledBackgroundColor: Colors.transparent, + backgroundColor: Colors.transparent), + child: child, + ), + ), + ); + }, + child: Text('Connect'), + ), + ), + ), + ], + ); + } +} diff --git a/lib/utils/nwc.dart b/lib/utils/nwc.dart index 53741dd..39637d8 100644 --- a/lib/utils/nwc.dart +++ b/lib/utils/nwc.dart @@ -2,15 +2,21 @@ import 'package:flutter_data/flutter_data.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:ndk/domain_layer/usecases/nwc/consts/nwc_method.dart'; import 'package:ndk/ndk.dart'; +import 'package:zapstore/main.data.dart'; +import 'package:zapstore/models/user.dart'; const kNwcSecretKey = 'nwc_secret'; class NwcConnectionNotifier extends StateNotifier> { + Ref ref; + late Ndk ndk; + final _storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), ); - NwcConnectionNotifier() : super(AsyncData(null)) { + NwcConnectionNotifier(this.ref) : super(AsyncLoading()) { + ndk = ref.users.nostrAdapter.socialRelays.ndk!; // Attempt to load from local storage on initialization _storage .read(key: kNwcSecretKey) @@ -32,13 +38,13 @@ class NwcConnectionNotifier extends StateNotifier> { // If secret is supplied and a connection is still active, // it means it is a new connection string so disconnect NWC if (state.value != null) { - await ndkForNwc.nwc.disconnect(state.value!); + await ndk.nwc.disconnect(state.value!); } // Write secret to storage and connect await _storage.write(key: kNwcSecretKey, value: nwcSecret); - final connection = await ndkForNwc.nwc.connect( + final connection = await ndk.nwc.connect( nwcSecret, doGetInfoMethod: false, onError: (error) { @@ -61,12 +67,4 @@ class NwcConnectionNotifier extends StateNotifier> { final nwcConnectionProvider = StateNotifierProvider>( - (ref) => NwcConnectionNotifier()); - -final ndkForNwc = Ndk( - NdkConfig( - cache: MemCacheManager(), - eventVerifier: Bip340EventVerifier(), - bootstrapRelays: [], - logLevel: LogLevels().info), -); + (ref) => NwcConnectionNotifier(ref)); diff --git a/lib/widgets/sign_in_container.dart b/lib/widgets/sign_in_container.dart index 554254b..ef90a43 100644 --- a/lib/widgets/sign_in_container.dart +++ b/lib/widgets/sign_in_container.dart @@ -67,7 +67,7 @@ class SignInButton extends ConsumerWidget { label, style: TextStyle(fontWeight: FontWeight.bold), ), - content: SignInContainer(), + content: SignInDialogBox(), ), ); } else { @@ -81,8 +81,9 @@ class SignInButton extends ConsumerWidget { } } -class SignInContainer extends HookConsumerWidget { - const SignInContainer({super.key}); +class SignInDialogBox extends HookConsumerWidget { + final bool publicKeyAllowed; + const SignInDialogBox({super.key, this.publicKeyAllowed = true}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -155,73 +156,81 @@ class SignInContainer extends HookConsumerWidget { settings.saveLocal(); if (context.mounted) { - Navigator.of(context).pop(); + Navigator.of(context).pop(user); } }, child: Text('Sign in with Amber'), ), - Gap(10), - Text('or'), - Gap(10), ], ), - Text('Input an npub or nostr address\n(read-only):'), - TextField( - autocorrect: false, - controller: controller, - onChanged: (value) { - isTextFieldEmpty.value = value.isEmpty; - }, - ), - Gap(10), - AsyncButtonBuilder( - disabled: isTextFieldEmpty.value, - loadingWidget: SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(), + if (amberSigner.isAvailable && publicKeyAllowed) + Padding( + padding: const EdgeInsets.all(10), + child: Text('or'), ), - onPressed: () async { - try { - final input = controller.text.trim(); - if (input.startsWith('nsec')) { - controller.clear(); - throw Exception('Never give away your nsec!'); - } - final user = await ref.users.findOne(input); - - final settings = ref.settings.findOneLocalById('_')!; - settings.signInMethod = SignInMethod.pubkey; - settings.user.value = user; - settings.saveLocal(); - - if (context.mounted) { - Navigator.pop(context); - } - } on Exception catch (e, stack) { - if (context.mounted) { - context.showError( - title: e.toString(), - description: stack.toString().substringMax(200)); - } - } - }, - builder: (context, child, callback, buttonState) { - return Padding( - padding: const EdgeInsets.only(bottom: 2), - child: SizedBox( - child: ElevatedButton( - onPressed: callback, - style: ElevatedButton.styleFrom( - disabledBackgroundColor: Colors.transparent, - backgroundColor: Colors.transparent), - child: child, - ), + if (publicKeyAllowed) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Input an npub or nostr address\n(read-only):'), + TextField( + autocorrect: false, + controller: controller, + onChanged: (value) { + isTextFieldEmpty.value = value.isEmpty; + }, ), - ); - }, - child: Text('Sign in with public key'), - ) + Gap(10), + AsyncButtonBuilder( + disabled: isTextFieldEmpty.value, + loadingWidget: SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(), + ), + onPressed: () async { + try { + final input = controller.text.trim(); + if (input.startsWith('nsec')) { + controller.clear(); + throw Exception('Never give away your nsec!'); + } + final user = await ref.users.findOne(input); + + final settings = ref.settings.findOneLocalById('_')!; + settings.signInMethod = SignInMethod.pubkey; + settings.user.value = user; + settings.saveLocal(); + + if (context.mounted) { + Navigator.pop(context); + } + } on Exception catch (e, stack) { + if (context.mounted) { + context.showError( + title: e.toString(), + description: stack.toString().substringMax(200)); + } + } + }, + builder: (context, child, callback, buttonState) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: SizedBox( + child: ElevatedButton( + onPressed: callback, + style: ElevatedButton.styleFrom( + disabledBackgroundColor: Colors.transparent, + backgroundColor: Colors.transparent), + child: child, + ), + ), + ); + }, + child: Text('Sign in with public key'), + ), + ], + ), ], ), ], diff --git a/lib/widgets/users_rich_text.dart b/lib/widgets/users_rich_text.dart new file mode 100644 index 0000000..2f0e5b6 --- /dev/null +++ b/lib/widgets/users_rich_text.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:zapstore/models/user.dart'; +import 'package:zapstore/widgets/rounded_image.dart'; + +class UsersRichText extends StatelessWidget { + const UsersRichText({ + super.key, + this.preSpan, + this.trailingText, + required this.users, + }); + + final TextSpan? preSpan; + final String? trailingText; + final List users; + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + children: [ + if (preSpan != null) preSpan!, + for (final user in users) + TextSpan( + style: TextStyle(height: 1.6), + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + RoundedImage(url: user.avatarUrl, size: 20), + Text( + ' ${user.nameOrNpub}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text(users.indexOf(user) == users.length - 1 + ? '' + : (users.indexOf(user) == users.length - 2 + ? ' and ' + : ', ')), + ], + ), + ), + if (users.indexOf(user) == users.length - 1) + TextSpan(text: trailingText, style: TextStyle(fontSize: 15)) + ], + ), + ], + ), + ); + } +} diff --git a/lib/widgets/wot_container.dart b/lib/widgets/wot_container.dart index 2e373ca..bd0e737 100644 --- a/lib/widgets/wot_container.dart +++ b/lib/widgets/wot_container.dart @@ -6,7 +6,7 @@ import 'package:purplebase/purplebase.dart' as base; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/settings.dart'; import 'package:zapstore/models/user.dart'; -import 'package:zapstore/widgets/rounded_image.dart'; +import 'package:zapstore/widgets/users_rich_text.dart'; class WebOfTrustContainer extends HookConsumerWidget { const WebOfTrustContainer({ @@ -68,56 +68,6 @@ class WebOfTrustContainer extends HookConsumerWidget { } } -class UsersRichText extends StatelessWidget { - const UsersRichText({ - super.key, - this.preSpan, - this.trailingText, - required this.users, - }); - - final TextSpan? preSpan; - final String? trailingText; - final List users; - - @override - Widget build(BuildContext context) { - return RichText( - text: TextSpan( - children: [ - if (preSpan != null) preSpan!, - for (final user in users) - TextSpan( - style: TextStyle(height: 1.6), - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - RoundedImage(url: user.avatarUrl, size: 20), - Text( - ' ${user.nameOrNpub}', - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(users.indexOf(user) == users.length - 1 - ? '' - : (users.indexOf(user) == users.length - 2 - ? ' and ' - : ', ')), - ], - ), - ), - if (users.indexOf(user) == users.length - 1) - TextSpan(text: trailingText, style: TextStyle(fontSize: 15)) - ], - ), - ], - ), - ); - } -} - final followsWhoFollowProvider = FutureProvider.autoDispose .family, ({String from, String to})>((ref, arg) async { final _ = diff --git a/lib/widgets/zap_button.dart b/lib/widgets/zap_button.dart index 8217f0c..f5f7067 100644 --- a/lib/widgets/zap_button.dart +++ b/lib/widgets/zap_button.dart @@ -1,12 +1,12 @@ -import 'package:async_button_builder/async_button_builder.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; import 'package:zapstore/models/settings.dart'; -import 'package:zapstore/navigation/router.dart'; +import 'package:zapstore/screens/settings_screen.dart'; import 'package:zapstore/utils/extensions.dart'; +import 'package:zapstore/widgets/sign_in_container.dart'; import '../utils/nwc.dart'; @@ -21,7 +21,6 @@ class ZapButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final nwcConnection = ref.watch(nwcConnectionProvider); - // final zapStatus = ref.watch(zapProvider); final user = ref.settings .watchOne('_', alsoWatch: (_) => {_.user}) .model! @@ -30,61 +29,45 @@ class ZapButton extends HookConsumerWidget { final amountController = TextEditingController(); - // We use value as default text, for progress only interested in loading/error substates - // final zapButtonText = switch (zapStatus) { - // AsyncLoading() => 'Zapping... ⚡⚡⚡', - // AsyncError() => 'Error zapping!', - // AsyncValue() => 'Zap the dev ⚡', - // }; - - // Show toast if could not zap - // useMemoized(() { - // WidgetsBinding.instance.addPostFrameCallback((_) { - // if (zapStatus.value != null) { - // // final receipt = await zapStatus.value!.zapReceipt; - // // TODO: zapStatus should include the amount of sats without needing async - // // I could read zapReceiptsNotifier's state, but how I know it's this one? - // context.showInfo('Zapped!', - // description: 'You just sent the dev some sats'); - // } - // if (zapStatus.hasError) { - // context.showError( - // title: 'Could not zap', - // description: 'Error sending zap: ${zapStatus.error}'); - // } - // }); - // }, [zapStatus.value, zapStatus.error]); - - return app.developer.value != null - ? AsyncButtonBuilder( - loadingWidget: SizedBox( - width: 14, height: 14, child: CircularProgressIndicator()), + return app.developer.value?.lud16 != null + ? ElevatedButton( onPressed: () async { - if (user == null) { - context.showError( - title: 'Not signed in', - description: 'Please sign in to zap'); + final loggedInUser = user ?? + await showDialog( + context: context, + builder: (_) => AlertDialog( + title: Text( + 'Sign in to zap', + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: SignInDialogBox(publicKeyAllowed: false), + ), + ); + + if (loggedInUser == null) { return; } if (nwcConnection.value == null) { - context.showError( - title: 'No wallet connected', - description: 'Please connect your NWC wallet'); - appRouter.go('/settings'); - return; + await showDialog( + // ignore: use_build_context_synchronously + context: context, + builder: (_) => AlertDialog( + title: Text( + 'Connect your wallet to zap', + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: NostrWalletConnectContainer(dialogMode: true), + ), + ); } - final developer = app.developer.value!; - if (developer.lud16 == null) { - context.showError( - title: 'Unable to zap', - description: - 'The developer (${developer.nameOrNpub}) does not have a Lightning address'); + if (ref.read(nwcConnectionProvider).value == null) { return; } final amount = await showDialog( + // ignore: use_build_context_synchronously context: context, builder: (context) { return AlertDialog( @@ -145,23 +128,18 @@ class ZapButton extends HookConsumerWidget { 0; if (amount > 0) { - // User should be able to sign any event - // Proposed API: - await user.zap(amount, - event: app.latestMetadata!, comment: 'From zapstore!'); - - // final lnurl = developer.lud16!; - // await ref.read(zapProvider.notifier).zap( - // user: user, - // lnurl: lnurl, - // eventId: app.latestMetadata!.id.toString(), - // amount: amount, - // pubKey: developer.pubkey); + try { + // ignore: use_build_context_synchronously + context.showInfo('Zap sent!', + description: '$amount sats on their way'); + await loggedInUser.zap(amount, event: app.latestMetadata!); + } catch (e) { + // ignore: use_build_context_synchronously + context.showError( + title: 'Unable to zap', description: e.toString()); + } } }, - builder: (context, child, callback, state) { - return ElevatedButton(onPressed: callback, child: child); - }, child: Text('Zap!'), ) : Container(); diff --git a/lib/widgets/zap_receipts.dart b/lib/widgets/zap_receipts.dart index 5728092..7f4b5ad 100644 --- a/lib/widgets/zap_receipts.dart +++ b/lib/widgets/zap_receipts.dart @@ -5,12 +5,11 @@ import 'package:ndk/domain_layer/entities/nip_01_event.dart'; import 'package:zapstore/main.data.dart'; import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:zapstore/models/file_metadata.dart'; import 'package:zapstore/models/user.dart'; import 'package:zapstore/models/zap_receipt.dart'; -import 'package:zapstore/widgets/rounded_image.dart'; +import 'package:zapstore/widgets/users_rich_text.dart'; class ZapReceipts extends HookConsumerWidget { ZapReceipts({ @@ -39,32 +38,10 @@ class ZapReceipts extends HookConsumerWidget { final senderIds = receipts.map((r) => r.senderPubkey).toSet(); final senders = ref.users.findManyLocalByIds(senderIds); - return Row( - children: [ - Text("⚡ $totalAmountInSats sats (${receipts.length} zaps)"), - Gap(5), - RichText( - text: TextSpan( - children: [ - for (final user in senders) - TextSpan( - style: TextStyle(height: 1.6), - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - RoundedImage(url: user.avatarUrl, size: 20), - ], - ), - ), - ], - ) - ], - ), - ) - ], + return UsersRichText( + preSpan: TextSpan(text: '⚡ $totalAmountInSats sats by'), + trailingText: ' and others', + users: senders, ); } } diff --git a/pubspec.lock b/pubspec.lock index e727d67..47c6921 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -810,10 +810,10 @@ packages: dependency: "direct main" description: name: ndk - sha256: e17cb3aad2936c62978891e0d3db4dc37f3ded7efbdf89e571c3927158da9135 + sha256: "07c284a14e7800513743a84c911a546dcfcdc0c0ce2def4895c4e4c2286f159e" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.4" octo_image: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 43bec19..56e6524 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: mutex: ^3.1.0 easy_image_viewer: ^1.5.1 flutter_secure_storage: ^9.2.2 - ndk: ^0.2.2 + ndk: ^0.2.4 dependency_overrides: purplebase: