diff --git a/lib/models/user.dart b/lib/models/user.dart index 407b234..fe076fb 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -247,6 +247,14 @@ mixin UserAdapter on NostrAdapter { } } +final signedInUserProvider = Provider((ref) { + return ref.settings + .watchOne('_', alsoWatch: (_) => {_.user}) + .model + ?.user + .value; +}); + const kZapstorePubkey = '78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d'; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e7bbcd3..a2aedf5 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -29,11 +29,7 @@ class SettingsScreen extends HookConsumerWidget { final systemInfoState = ref.watch(systemInfoNotifierProvider); final feedbackController = useTextEditingController(); - final user = ref.settings - .watchOne('_', alsoWatch: (_) => {_.user}) - .model! - .user - .value; + final signedInUser = ref.watch(signedInUserProvider); return SingleChildScrollView( child: Column( @@ -57,16 +53,17 @@ class SettingsScreen extends HookConsumerWidget { maxLines: 10, ), Gap(20), - if (user == null) SignInButton(minimal: true), - if (user != null && - user.settings.value!.signInMethod != SignInMethod.nip55 || + if (signedInUser == null) SignInButton(minimal: true), + if (signedInUser != null && + signedInUser.settings.value!.signInMethod != + SignInMethod.nip55 || !amberSigner.isAvailable) Text( 'To share feedback, you must be signed in with Amber. You can also message us on nostr!', style: TextStyle( color: Colors.red[300], fontWeight: FontWeight.bold)), - if (user != null && - user.settings.value!.signInMethod == SignInMethod.nip55 && + if (signedInUser != null && + signedInUser.settings.value!.signInMethod == SignInMethod.nip55 && amberSigner.isAvailable) AsyncButtonBuilder( loadingWidget: SmallCircularProgressIndicator(), @@ -76,7 +73,7 @@ class SettingsScreen extends HookConsumerWidget { PartialDirectMessage( content: feedbackController.text.trim(), receiver: kZapstorePubkey.npub), - withPubkey: user.pubkey); + withPubkey: signedInUser.pubkey); try { final response = await http.post( Uri.parse('https://relay.zapstore.dev/'), @@ -98,15 +95,9 @@ class SettingsScreen extends HookConsumerWidget { } } }, - builder: (context, child, callback, state) { - return switch (state) { - _ => ElevatedButton( - onPressed: callback, - child: child, - ), - }; - }, - child: Text('Send as ${user.nameOrNpub}'), + builder: (context, child, callback, state) => + ElevatedButton(onPressed: callback, child: child), + child: Text('Send as ${signedInUser.nameOrNpub}'), ), Gap(20), Divider(), diff --git a/lib/widgets/install_alert_dialog.dart b/lib/widgets/install_alert_dialog.dart index d585b1c..77e555a 100644 --- a/lib/widgets/install_alert_dialog.dart +++ b/lib/widgets/install_alert_dialog.dart @@ -3,9 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:purplebase/purplebase.dart' as base; -import 'package:zapstore/main.data.dart'; import 'package:zapstore/models/app.dart'; -import 'package:zapstore/models/settings.dart'; import 'package:zapstore/models/user.dart'; import 'package:zapstore/widgets/sign_in_container.dart'; import 'package:zapstore/widgets/author_container.dart'; @@ -22,11 +20,7 @@ class InstallAlertDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final trustedSignerNotifier = useState(false); - final user = ref.settings - .watchOne('_', alsoWatch: (_) => {_.user}) - .model! - .user - .value; + final signedInUser = ref.watch(signedInUserProvider); return AlertDialog( elevation: 10, @@ -50,10 +44,10 @@ class InstallAlertDialog extends HookConsumerWidget { Gap(20), if (app.signer.value != null) WebOfTrustContainer( - fromNpub: user?.npub ?? kFranzapPubkey.npub, + fromNpub: signedInUser?.npub ?? kFranzapPubkey.npub, toNpub: app.signer.value!.npub, ), - if (user == null) + if (signedInUser == null) SignInButton( label: 'Sign in to view your web of trust', minimal: true, diff --git a/lib/widgets/sign_in_container.dart b/lib/widgets/sign_in_container.dart index a362034..96051b9 100644 --- a/lib/widgets/sign_in_container.dart +++ b/lib/widgets/sign_in_container.dart @@ -26,11 +26,7 @@ class SignInButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final user = ref.settings - .watchOne('_', alsoWatch: (_) => {_.user}) - .model - ?.user - .value; + final user = ref.watch(signedInUserProvider); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/widgets/user_avatar.dart b/lib/widgets/user_avatar.dart index db2797a..2445004 100644 --- a/lib/widgets/user_avatar.dart +++ b/lib/widgets/user_avatar.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -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'; class UserAvatar extends HookConsumerWidget { @@ -9,11 +8,7 @@ class UserAvatar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final user = ref.settings - .watchOne('_', alsoWatch: (_) => {_.user}) - .model! - .user - .value; - return RoundedImage(url: user?.avatarUrl, size: 46); + final signedInUser = ref.watch(signedInUserProvider); + return RoundedImage(url: signedInUser?.avatarUrl, size: 46); } } diff --git a/lib/widgets/users_rich_text.dart b/lib/widgets/users_rich_text.dart index cefc469..b8a17a7 100644 --- a/lib/widgets/users_rich_text.dart +++ b/lib/widgets/users_rich_text.dart @@ -3,54 +3,74 @@ import 'package:zapstore/models/user.dart'; import 'package:zapstore/widgets/rounded_image.dart'; class UsersRichText extends StatelessWidget { - const UsersRichText({ - super.key, - this.leadingText, - this.trailingText, - required this.users, - }); - - final String? leadingText; + final TextSpan? leadingTextSpan; final String? trailingText; final List users; + final User? signedInUser; + final int? maxUsersToDisplay; + final double fontSize; + + const UsersRichText({ + super.key, + this.leadingTextSpan, + this.trailingText, + required this.users, + this.signedInUser, + this.maxUsersToDisplay, + this.fontSize = 16, + }); @override Widget build(BuildContext context) { + // Move signed in user to head of list + if (users.remove(signedInUser)) { + users.insert(0, signedInUser!); + } + final usersToDisplay = maxUsersToDisplay != null + ? users.take(maxUsersToDisplay!).toList() + : users; + return RichText( text: TextSpan( children: [ - if (leadingText != null) - TextSpan(text: leadingText!, style: TextStyle(fontSize: 15)), - for (final user in users) - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Wrap( - spacing: 1, - runSpacing: 10, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - RoundedImage(url: user.avatarUrl, size: 20), - Text( - ' ${user.nameOrNpub}', - style: TextStyle( - fontSize: 15, fontWeight: FontWeight.bold), - ), - Text( - users.indexOf(user) == users.length - 1 - ? '' - : (users.indexOf(user) == users.length - 2 - ? ' and ' - : ', '), - style: TextStyle(fontSize: 15), - ), - ], + for (final user in usersToDisplay) + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Wrap( + spacing: 0, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (usersToDisplay.indexOf(user) == 0 && + leadingTextSpan != null) + RichText(text: leadingTextSpan!), + if (user != signedInUser) + Padding( + padding: const EdgeInsets.only(left: 2, right: 2), + child: RoundedImage(url: user.avatarUrl, size: 20), + ), + Text( + user == signedInUser + ? (leadingTextSpan != null ? ' you' : 'You') + : ' ${user.nameOrNpub}', + style: TextStyle( + fontSize: fontSize, fontWeight: FontWeight.bold), ), - ), - if (users.indexOf(user) == users.length - 1) - TextSpan(text: trailingText, style: TextStyle(fontSize: 15)) - ], + Text( + usersToDisplay.indexOf(user) == usersToDisplay.length - 1 + ? '' + : (usersToDisplay.indexOf(user) == + usersToDisplay.length - 2 + ? usersToDisplay.length < users.length + ? ', ' + : ' and ' + : ', '), + style: TextStyle(fontSize: fontSize)), + if (usersToDisplay.indexOf(user) == usersToDisplay.length - 1) + Text( + '${usersToDisplay.length < users.length ? ' and ${users.length - usersToDisplay.length} others' : ''}${trailingText ?? ''}', + style: TextStyle(fontSize: fontSize)) + ], + ), ), ], ), diff --git a/lib/widgets/wot_container.dart b/lib/widgets/wot_container.dart index 2b01c4c..9476c75 100644 --- a/lib/widgets/wot_container.dart +++ b/lib/widgets/wot_container.dart @@ -1,10 +1,7 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -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/screens/settings_screen.dart'; import 'package:zapstore/widgets/users_rich_text.dart'; @@ -23,25 +20,20 @@ class WebOfTrustContainer extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final data = ref.watch(followsWhoFollowProvider((from: fromNpub, to: toNpub))); + final signedInUser = ref.watch(signedInUserProvider); return switch (data) { AsyncData>(value: final trustedUsers) => Builder( builder: (context) { - final hasUser = - trustedUsers.firstWhereOrNull((u) => u.npub == fromNpub) != - null; - final trustedUsersWithoutLoggedInUser = - trustedUsers.where((u) => u.npub != fromNpub).toList(); - if (trustedUsersWithoutLoggedInUser.isEmpty) { + if (trustedUsers.isEmpty) { return Text( 'No trusted users between you and the signer. (This may be a service error)'); } return UsersRichText( - leadingText: - hasUser && fromNpub != kFranzapPubkey.npub ? 'You, ' : null, + users: trustedUsers, + signedInUser: signedInUser, trailingText: ' and others follow this signer on nostr.', - users: trustedUsersWithoutLoggedInUser, ); }, ), @@ -66,8 +58,7 @@ class WebOfTrustContainer extends HookConsumerWidget { final followsWhoFollowProvider = FutureProvider.autoDispose .family, ({String from, String to})>((ref, arg) async { - final _ = - ref.settings.watchOne('_', alsoWatch: (_) => {_.user}).model!.user.value; + final _ = ref.watch(signedInUserProvider); final users = await ref.users.userAdapter.getTrusted(arg.from, arg.to); return users.toSet().toList(); }); diff --git a/lib/widgets/zap_button.dart b/lib/widgets/zap_button.dart index 7c7fd7d..53346cb 100644 --- a/lib/widgets/zap_button.dart +++ b/lib/widgets/zap_button.dart @@ -1,9 +1,10 @@ +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/models/user.dart'; import 'package:zapstore/navigation/app_initializer.dart'; import 'package:zapstore/screens/settings_screen.dart'; import 'package:zapstore/utils/extensions.dart'; @@ -30,7 +31,16 @@ class ZapButton extends HookConsumerWidget { return Container(); } - return ElevatedButton( + return AsyncButtonBuilder( + loadingWidget: Text('⚡️ Zapping...').bold, + builder: (context, child, callback, state) => ElevatedButton( + onPressed: callback, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 60, 60, 54), + disabledBackgroundColor: Colors.grey[600], + ), + child: child, + ), onPressed: nwcConnection.isLoading ? null : () async { @@ -59,13 +69,10 @@ class ZapButton extends HookConsumerWidget { children: [ Consumer( builder: (context, ref, _) { - final user = ref.settings - .watchOne('_', alsoWatch: (_) => {_.user}) - .model! - .user - .value; + final signedInUser = + ref.watch(signedInUserProvider); - if (user == null) { + if (signedInUser == null) { return Column( children: [ Text( @@ -141,10 +148,10 @@ class ZapButton extends HookConsumerWidget { if (amount > 0) { try { // Reload user as we are in a callback and it could have changed - final loggedInUser = + final signedInUser = ref.settings.findOneLocalById('_')!.user.value; - if (loggedInUser != null) { - await loggedInUser.zap(amount, + if (signedInUser != null) { + await signedInUser.zap(amount, event: app.latestMetadata!, comment: comment); } else { await anonUser!.zap(amount, @@ -161,10 +168,6 @@ class ZapButton extends HookConsumerWidget { } } }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey[800], - disabledBackgroundColor: Colors.grey[500], - ), child: Text( '⚡️ Zap this release', style: TextStyle( diff --git a/lib/widgets/zap_receipts.dart b/lib/widgets/zap_receipts.dart index cbb9243..f22e8ce 100644 --- a/lib/widgets/zap_receipts.dart +++ b/lib/widgets/zap_receipts.dart @@ -25,6 +25,8 @@ class ZapReceipts extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final signedInUser = ref.watch(signedInUserProvider); + // NOTE: These watchers will become more efficient ref.users.watchAll(); @@ -47,14 +49,25 @@ class ZapReceipts extends HookConsumerWidget { final receipts = zapReceipts.value!; final totalAmountInSats = receipts.fold(0, (acc, e) => acc + e.amount); - final senderIds = receipts.map((r) => r.senderPubkey).toSet(); - final senders = ref.users.findManyLocalByIds(senderIds); + final zapperIds = receipts.map((r) => r.senderPubkey).toSet(); + final zappers = ref.users.findManyLocalByIds(zapperIds); return Padding( padding: const EdgeInsets.only(top: 10, bottom: 10), child: UsersRichText( - leadingText: '⚡ $totalAmountInSats sats zapped from ', - users: senders, + leadingTextSpan: TextSpan( + style: TextStyle(fontSize: 16), + children: [ + TextSpan(text: '⚡'), + TextSpan( + text: ' $totalAmountInSats sats ', + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: 'zapped by'), + ], + ), + users: zappers, + signedInUser: signedInUser, + maxUsersToDisplay: 5, ), ); }