From 6739eac19dd2cdbee32baa93e13e6db1b0398aec Mon Sep 17 00:00:00 2001 From: ice-kreios <180917405+ice-kreios@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:25:55 +0200 Subject: [PATCH] feat: shared user picker modal (#517) ## Description This PR introduces a new reusable UserPickerSheet component to standardize user selection across the app. ## Additional Notes N/A ## Type of Change - [x] Bug fix - [x] New feature - [ ] Breaking change - [x] Refactoring - [ ] Documentation - [ ] Chore ## Screenshots (if applicable) image image image image image image --- .../following_user_list.dart | 47 -------- .../components/more_content_view.dart | 11 +- .../add_admin_modal/add_admin_modal.dart | 102 +++++++----------- .../pages/new_chat_modal/new_chat_modal.dart | 88 ++++++--------- .../pages/add_group_participants_modal.dart | 99 +++++++---------- .../share_profile_modal.dart | 28 +++++ .../contacts/pages/contacts_list_view.dart | 97 ----------------- .../components/follower_users.dart | 60 +++++++++++ .../components/following_users.dart | 46 ++++++++ .../components/no_user_view.dart | 43 ++++++++ .../components/searched_users.dart} | 29 +++-- .../components/selectable_user_list_item.dart | 54 ++++++++++ .../user_picker_sheet/user_picker_sheet.dart | 92 ++++++++++++++++ .../search_users_data_source_provider.c.dart} | 8 +- .../components/send_coins_form.dart | 8 +- .../pages/friends_modal/friends_modal.dart | 27 +++++ .../pages/send_nft_form/send_nft_form.dart | 8 +- .../contacts/contacts_list_header.dart | 16 +-- lib/app/router/app_routes.c.dart | 3 +- lib/app/router/chat_routes.dart | 10 +- lib/app/router/wallet_routes.dart | 29 ++--- lib/l10n/app_en.arb | 5 +- 22 files changed, 513 insertions(+), 397 deletions(-) delete mode 100644 lib/app/features/chat/components/following_user_list/following_user_list.dart create mode 100644 lib/app/features/chat/views/pages/share_profile_modal/share_profile_modal.dart delete mode 100644 lib/app/features/contacts/pages/contacts_list_view.dart create mode 100644 lib/app/features/user/pages/user_picker_sheet/components/follower_users.dart create mode 100644 lib/app/features/user/pages/user_picker_sheet/components/following_users.dart create mode 100644 lib/app/features/user/pages/user_picker_sheet/components/no_user_view.dart rename lib/app/features/{chat/components/searched_user_list/searched_user_list.dart => user/pages/user_picker_sheet/components/searched_users.dart} (64%) create mode 100644 lib/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart create mode 100644 lib/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart rename lib/app/features/{chat/providers/users_data_source_provider.c.dart => user/providers/search_users_data_source_provider.c.dart} (80%) create mode 100644 lib/app/features/wallet/views/pages/friends_modal/friends_modal.dart diff --git a/lib/app/features/chat/components/following_user_list/following_user_list.dart b/lib/app/features/chat/components/following_user_list/following_user_list.dart deleted file mode 100644 index 2be1228b6..000000000 --- a/lib/app/features/chat/components/following_user_list/following_user_list.dart +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/components/list_item/list_item.dart'; -import 'package:ion/app/features/chat/views/pages/new_chat_modal/components/new_chat_initial_view/new_chat_initial_view.dart'; -import 'package:ion/app/features/user/providers/follow_list_provider.c.dart'; -import 'package:ion/app/features/user/providers/user_metadata_provider.c.dart'; -import 'package:ion/app/utils/username.dart'; - -class FollowingUsersList extends ConsumerWidget { - const FollowingUsersList({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final following = ref.watch(currentUserFollowListProvider); - final followers = following.valueOrNull?.data.list; - final pubkeys = followers?.map((e) => e.pubkey).toList() ?? []; - - if (pubkeys.isEmpty) { - return const NewChatInitialView(); - } - - return ListView.builder( - itemBuilder: (context, index) { - return _FollowingUserListItem(pubkey: pubkeys[index]); - }, - itemCount: pubkeys.length, - ); - } -} - -class _FollowingUserListItem extends ConsumerWidget { - const _FollowingUserListItem({required this.pubkey}); - - final String pubkey; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(userMetadataProvider(pubkey)).valueOrNull; - return ListItem.user( - title: Text(user?.data.displayName ?? ''), - subtitle: Text(prefixUsername(username: user?.data.name ?? '', context: context)), - profilePicture: user?.data.picture, - ); - } -} diff --git a/lib/app/features/chat/messages/views/components/messaging_bottom_bar/components/more_content_view.dart b/lib/app/features/chat/messages/views/components/messaging_bottom_bar/components/more_content_view.dart index 52b2fb0d8..a58ab43f1 100644 --- a/lib/app/features/chat/messages/views/components/messaging_bottom_bar/components/more_content_view.dart +++ b/lib/app/features/chat/messages/views/components/messaging_bottom_bar/components/more_content_view.dart @@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/extensions/extensions.dart'; import 'package:ion/app/features/chat/providers/messaging_bottom_bar_state_provider.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; -import 'package:ion/app/services/logger/logger.dart'; import 'package:ion/generated/assets.gen.dart'; final double moreContentHeight = 206.0.s; @@ -48,14 +47,8 @@ class MoreContentView extends ConsumerWidget { _MoreContentItem( iconPath: Assets.svg.walletChatPerson, title: context.i18n.common_profile, - onTap: () async { - final contactId = await ShareProfileModalRoute( - title: context.i18n.chat_profile_share_modal_title, - ).push(context); - - //TODO: use contactId to share profile - Logger.log(contactId ?? 'No contact selected'); - + onTap: () { + ShareProfileModalRoute().push(context); ref.read(messagingBottomBarActiveStateProvider.notifier).setText(); }, ), diff --git a/lib/app/features/chat/views/pages/new_channel_modal/pages/add_admin_modal/add_admin_modal.dart b/lib/app/features/chat/views/pages/new_channel_modal/pages/add_admin_modal/add_admin_modal.dart index cf7654e5e..86c2e6c53 100644 --- a/lib/app/features/chat/views/pages/new_channel_modal/pages/add_admin_modal/add_admin_modal.dart +++ b/lib/app/features/chat/views/pages/new_channel_modal/pages/add_admin_modal/add_admin_modal.dart @@ -7,12 +7,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/button/button.dart'; import 'package:ion/app/components/separated/separator.dart'; import 'package:ion/app/extensions/extensions.dart'; -import 'package:ion/app/features/auth/providers/content_creators_data_source_provider.c.dart'; import 'package:ion/app/features/chat/model/channel_admin_type.dart'; import 'package:ion/app/features/chat/providers/channel_admins_provider.c.dart'; -import 'package:ion/app/features/chat/views/components/selectable_user_list.dart'; -import 'package:ion/app/features/nostr/providers/entities_paged_data_provider.c.dart'; import 'package:ion/app/features/user/model/user_metadata.c.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; import 'package:ion/generated/assets.gen.dart'; class AddAdminModal extends HookConsumerWidget { @@ -22,73 +22,49 @@ class AddAdminModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final searchValue = useState(''); final selectedPubkey = useState(null); - final dataSource = ref.watch(contentCreatorsDataSourceProvider); - final entitiesPagedData = ref.watch(entitiesPagedDataProvider(dataSource)); - final contentCreators = entitiesPagedData?.data.items; - - final isLoading = contentCreators?.isEmpty ?? true; - final userEntries = useMemoized( - () => (contentCreators - ?.whereType() - .where( - (entity) => - entity.data.displayName - .toLowerCase() - .contains(searchValue.value.toLowerCase()) || - entity.data.name.toLowerCase().contains(searchValue.value.toLowerCase()), - ) - .toList() ?? - []) - ..sort( - (a, b) => a.data.displayName.toLowerCase().compareTo(b.data.displayName.toLowerCase()), - ), - [contentCreators, searchValue.value], - ); - return SizedBox( height: MediaQuery.sizeOf(context).height * 0.8, - child: Column( - children: [ - Expanded( - child: SelectableUserList( - title: context.i18n.channel_create_admins_action, - isLoading: isLoading, - selected: [ - if (selectedPubkey.value != null) selectedPubkey.value!, - ], - userEntries: userEntries, - onSelect: (String value) => selectedPubkey.value = value, - onSearchValueChanged: (String value) => searchValue.value = value, - ), - ), - const HorizontalSeparator(), - Padding( - padding: EdgeInsets.symmetric( - vertical: 16.0.s, - horizontal: 44.0.s, - ), - child: Button( - type: selectedPubkey.value == null ? ButtonType.disabled : ButtonType.primary, - mainAxisSize: MainAxisSize.max, - minimumSize: Size(56.0.s, 56.0.s), - leadingIcon: Assets.svg.iconProfileSave.icon( - color: context.theme.appColors.onPrimaryAccent, + child: UserPickerSheet( + onUserSelected: (UserMetadataEntity user) => selectedPubkey.value = user.masterPubkey, + selectedPubkeys: selectedPubkey.value != null ? [selectedPubkey.value!] : null, + selectable: true, + navigationBar: NavigationAppBar.modal( + title: Text(context.i18n.channel_create_admins_action), + showBackButton: false, + actions: const [ + NavigationCloseButton(), + ], + ), + bottomContent: Column( + children: [ + const HorizontalSeparator(), + Padding( + padding: EdgeInsets.symmetric( + vertical: 16.0.s, + horizontal: 44.0.s, ), - label: Text( - context.i18n.button_confirm, + child: Button( + type: selectedPubkey.value == null ? ButtonType.disabled : ButtonType.primary, + mainAxisSize: MainAxisSize.max, + minimumSize: Size(56.0.s, 56.0.s), + leadingIcon: Assets.svg.iconProfileSave.icon( + color: context.theme.appColors.onPrimaryAccent, + ), + label: Text( + context.i18n.button_confirm, + ), + onPressed: () { + ref + .read(channelAdminsProvider().notifier) + .setAdmin(selectedPubkey.value!, ChannelAdminType.admin); + context.pop(); + }, ), - onPressed: () { - ref - .read(channelAdminsProvider().notifier) - .setAdmin(selectedPubkey.value!, ChannelAdminType.admin); - context.pop(); - }, ), - ), - ], + ], + ), ), ); } diff --git a/lib/app/features/chat/views/pages/new_chat_modal/new_chat_modal.dart b/lib/app/features/chat/views/pages/new_chat_modal/new_chat_modal.dart index 16563d497..20befd4b8 100644 --- a/lib/app/features/chat/views/pages/new_chat_modal/new_chat_modal.dart +++ b/lib/app/features/chat/views/pages/new_chat_modal/new_chat_modal.dart @@ -1,78 +1,50 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:ion/app/components/button/button.dart'; -import 'package:ion/app/components/inputs/search_input/search_input.dart'; -import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/extensions/extensions.dart'; -import 'package:ion/app/features/chat/components/following_user_list/following_user_list.dart'; -import 'package:ion/app/features/chat/components/searched_user_list/searched_user_list.dart'; -import 'package:ion/app/features/chat/providers/users_data_source_provider.c.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart'; import 'package:ion/app/router/app_routes.c.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; import 'package:ion/generated/assets.gen.dart'; -class NewChatModal extends HookConsumerWidget { +class NewChatModal extends StatelessWidget { const NewChatModal({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final searchText = ref.watch(usersSearchTextProvider); - + Widget build(BuildContext context) { return SheetContent( topPadding: 0, - body: Column( - children: [ - NavigationAppBar.modal( - showBackButton: false, - title: Text(context.i18n.new_chat_modal_title), - actions: const [NavigationCloseButton()], - ), - SizedBox(height: 9.0.s), - Expanded( - child: ScreenSideOffset.small( - child: Column( - children: [ - SearchInput( - textInputAction: TextInputAction.search, - onTextChanged: (text) { - ref.read(usersSearchTextProvider.notifier).text = text; - }, - ), - SizedBox(height: 12.0.s), - Row( - children: [ - _HeaderButton( - icon: Assets.svg.iconSearchGroups, - title: context.i18n.new_chat_modal_new_group_button, - onTap: () { - AddParticipantsToGroupModalRoute().push(context); - }, - ), - SizedBox(width: 20.0.s), - _HeaderButton( - icon: Assets.svg.iconSearchChannel, - title: context.i18n.new_chat_modal_new_channel_button, - onTap: () { - NewChannelModalRoute().replace(context); - }, - ), - ], - ), - SizedBox(height: 12.0.s), - //TODO: update user list when figma design is ready - Expanded( - child: - searchText.isEmpty ? const FollowingUsersList() : const SearchedUsersList(), - ), - ], - ), + body: UserPickerSheet( + navigationBar: NavigationAppBar.modal( + showBackButton: false, + title: Text(context.i18n.new_chat_modal_title), + actions: const [NavigationCloseButton()], + ), + initialUserListType: UserListType.follower, + onUserSelected: (_) => context.pop(), + header: Row( + children: [ + _HeaderButton( + icon: Assets.svg.iconSearchGroups, + title: context.i18n.new_chat_modal_new_group_button, + onTap: () { + AddParticipantsToGroupModalRoute().push(context); + }, ), - ), - ], + SizedBox(width: 20.0.s), + _HeaderButton( + icon: Assets.svg.iconSearchChannel, + title: context.i18n.new_chat_modal_new_channel_button, + onTap: () { + NewChannelModalRoute().replace(context); + }, + ), + ], + ), ), ); } diff --git a/lib/app/features/chat/views/pages/new_group_modal/pages/add_group_participants_modal.dart b/lib/app/features/chat/views/pages/new_group_modal/pages/add_group_participants_modal.dart index e6dbffbd1..37f926651 100644 --- a/lib/app/features/chat/views/pages/new_group_modal/pages/add_group_participants_modal.dart +++ b/lib/app/features/chat/views/pages/new_group_modal/pages/add_group_participants_modal.dart @@ -1,19 +1,17 @@ // SPDX-License-Identifier: ice License 1.0 import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:ion/app/components/button/button.dart'; import 'package:ion/app/components/screen_offset/screen_bottom_offset.dart'; import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/components/separated/separator.dart'; import 'package:ion/app/extensions/extensions.dart'; -import 'package:ion/app/features/auth/providers/content_creators_data_source_provider.c.dart'; import 'package:ion/app/features/chat/providers/create_group_form_controller_provider.c.dart'; -import 'package:ion/app/features/chat/views/components/selectable_user_list.dart'; -import 'package:ion/app/features/nostr/providers/entities_paged_data_provider.c.dart'; -import 'package:ion/app/features/user/model/user_metadata.c.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart'; import 'package:ion/app/router/app_routes.c.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; import 'package:ion/generated/assets.gen.dart'; @@ -23,69 +21,48 @@ class AddGroupParticipantsModal extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final createGroupForm = ref.watch(createGroupFormControllerProvider); - final createGroupFormNotifier = ref.read(createGroupFormControllerProvider.notifier); - - final searchValue = useState(''); - - final dataSource = ref.watch(contentCreatorsDataSourceProvider); - final entitiesPagedData = ref.watch(entitiesPagedDataProvider(dataSource)); - final contentCreators = entitiesPagedData?.data.items; - - final isLoading = contentCreators?.isEmpty ?? true; - - // TODO: Replace stub with implemented search - final userEntries = useMemoized( - () => (contentCreators - ?.whereType() - .where( - (entity) => - entity.data.displayName - .toLowerCase() - .contains(searchValue.value.toLowerCase()) || - entity.data.name.toLowerCase().contains(searchValue.value.toLowerCase()), - ) - .toList() ?? - []) - ..sort( - (a, b) => a.data.displayName.toLowerCase().compareTo(b.data.displayName.toLowerCase()), - ), - [contentCreators, searchValue.value], - ); + final createGroupFormNotifier = ref.watch(createGroupFormControllerProvider.notifier); return SheetContent( topPadding: 0, - body: Column( - children: [ - Expanded( - child: SelectableUserList( - title: context.i18n.group_create_title, - isLoading: isLoading, - userEntries: userEntries, - selected: createGroupForm.members.toList(), - onSelect: createGroupFormNotifier.toggleMember, - onSearchValueChanged: (String value) => searchValue.value = value, - ), - ), - const HorizontalSeparator(), - ScreenBottomOffset( - margin: 32.0.s, - child: Padding( - padding: EdgeInsets.only(top: 16.0.s), - child: ScreenSideOffset.large( - child: Button( - onPressed: () { - CreateGroupModalRoute().push(context); - }, - label: Text(context.i18n.button_next), - mainAxisSize: MainAxisSize.max, - trailingIcon: Assets.svg.iconButtonNext.icon( - color: context.theme.appColors.onPrimaryAccent, + body: UserPickerSheet( + key: const Key('add-group-participants-modal'), + selectedPubkeys: createGroupForm.members.toList(), + selectable: true, + initialUserListType: UserListType.follower, + onUserSelected: (user) { + createGroupFormNotifier.toggleMember(user.masterPubkey); + }, + navigationBar: NavigationAppBar.modal( + title: Text(context.i18n.group_create_title), + showBackButton: false, + actions: const [ + NavigationCloseButton(), + ], + ), + bottomContent: Column( + children: [ + const HorizontalSeparator(), + ScreenBottomOffset( + margin: 32.0.s, + child: Padding( + padding: EdgeInsets.only(top: 16.0.s), + child: ScreenSideOffset.large( + child: Button( + onPressed: () { + CreateGroupModalRoute().push(context); + }, + label: Text(context.i18n.button_next), + mainAxisSize: MainAxisSize.max, + trailingIcon: Assets.svg.iconButtonNext.icon( + color: context.theme.appColors.onPrimaryAccent, + ), ), ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/app/features/chat/views/pages/share_profile_modal/share_profile_modal.dart b/lib/app/features/chat/views/pages/share_profile_modal/share_profile_modal.dart new file mode 100644 index 000000000..1312ba145 --- /dev/null +++ b/lib/app/features/chat/views/pages/share_profile_modal/share_profile_modal.dart @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; +import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; + +class ShareProfileModal extends StatelessWidget { + const ShareProfileModal({super.key}); + + @override + Widget build(BuildContext context) { + return SheetContent( + topPadding: 0, + body: UserPickerSheet( + navigationBar: NavigationAppBar.modal( + showBackButton: false, + title: Text(context.i18n.chat_profile_share_modal_title), + actions: [NavigationCloseButton(onPressed: () => context.pop())], + ), + onUserSelected: (_) => context.pop(), + ), + ); + } +} diff --git a/lib/app/features/contacts/pages/contacts_list_view.dart b/lib/app/features/contacts/pages/contacts_list_view.dart deleted file mode 100644 index 99b019974..000000000 --- a/lib/app/features/contacts/pages/contacts_list_view.dart +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/components/inputs/search_input/search_input.dart'; -import 'package:ion/app/components/list_item/list_item.dart'; -import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; -import 'package:ion/app/extensions/extensions.dart'; -import 'package:ion/app/features/contacts/providers/contacts_provider.c.dart'; -import 'package:ion/app/router/app_routes.c.dart'; -import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; -import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; -import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; - -enum ContactRouteAction { - navigate, - pop, -} - -class ContactsListView extends ConsumerWidget { - const ContactsListView({ - required this.appBarTitle, - required this.action, - this.showBackButton = false, - super.key, - }); - - final String appBarTitle; - final ContactRouteAction action; - final bool showBackButton; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final contacts = ref.watch(contactsProvider); - - return SheetContent( - body: CustomScrollView( - slivers: [ - SliverAppBar( - primary: false, - flexibleSpace: NavigationAppBar.modal( - showBackButton: showBackButton, - actions: [ - NavigationCloseButton( - onPressed: () => context.pop(), - ), - ], - title: Text(appBarTitle), - ), - automaticallyImplyLeading: false, - toolbarHeight: NavigationAppBar.modalHeaderHeight, - pinned: true, - ), - PinnedHeaderSliver( - child: ColoredBox( - color: context.theme.appColors.onPrimaryAccent, - child: Column( - children: [ - SizedBox(height: 16.0.s), - ScreenSideOffset.small( - child: SearchInput( - onTextChanged: (String value) {}, - ), - ), - SizedBox(height: 16.0.s), - ], - ), - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - final contact = contacts[index]; - return ScreenSideOffset.small( - child: Padding( - padding: EdgeInsets.only(bottom: 12.0.s), - child: ListItem.user( - title: Text(contact.name), - subtitle: Text(contact.nickname!), - profilePicture: contact.icon, - timeago: contact.lastSeen, - onTap: () => action == ContactRouteAction.pop - ? context.pop(contact.id) - : ContactRoute(contactId: contact.id).push(context), - ), - ), - ); - }, - childCount: contacts.length, - ), - ), - ], - ), - ); - } -} diff --git a/lib/app/features/user/pages/user_picker_sheet/components/follower_users.dart b/lib/app/features/user/pages/user_picker_sheet/components/follower_users.dart new file mode 100644 index 000000000..e2d75790c --- /dev/null +++ b/lib/app/features/user/pages/user_picker_sheet/components/follower_users.dart @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/components/scroll_view/load_more_builder.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/auth/providers/auth_provider.c.dart'; +import 'package:ion/app/features/nostr/providers/entities_paged_data_provider.c.dart'; +import 'package:ion/app/features/user/model/user_metadata.c.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart'; +import 'package:ion/app/features/user/providers/followers_data_source_provider.c.dart'; + +class FollowerUsers extends ConsumerWidget { + const FollowerUsers({ + required this.onUserSelected, + this.selectable = false, + this.selectedPubkeys, + super.key, + }); + + final void Function(UserMetadataEntity user) onUserSelected; + final bool selectable; + final List? selectedPubkeys; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final pubkey = ref.watch(currentPubkeySelectorProvider).valueOrNull; + final dataSource = ref.watch(followersDataSourceProvider(pubkey!)); + final entitiesPagedData = ref.watch(entitiesPagedDataProvider(dataSource)); + final users = entitiesPagedData?.data.items?.whereType().toList(); + final slivers = [ + if (users == null || users.isEmpty) + const SliverToBoxAdapter(child: SizedBox.shrink()) + else + SliverList.separated( + separatorBuilder: (BuildContext _, int __) => SizedBox(height: 8.0.s), + itemCount: users.length, + itemBuilder: (BuildContext context, int index) { + final user = users.elementAt(index); + return SelectableUserListItem( + pubkey: user.masterPubkey, + onUserSelected: onUserSelected, + selectedPubkeys: selectedPubkeys, + selectable: selectable, + ); + }, + ), + ]; + + return LoadMoreBuilder( + slivers: slivers, + builder: (context, slivers) => CustomScrollView( + key: const ValueKey('paged_followers_scroll_view'), + slivers: slivers, + ), + onLoadMore: ref.read(entitiesPagedDataProvider(dataSource).notifier).fetchEntities, + hasMore: entitiesPagedData?.hasMore ?? true, + ); + } +} diff --git a/lib/app/features/user/pages/user_picker_sheet/components/following_users.dart b/lib/app/features/user/pages/user_picker_sheet/components/following_users.dart new file mode 100644 index 000000000..9a68064db --- /dev/null +++ b/lib/app/features/user/pages/user_picker_sheet/components/following_users.dart @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/user/model/user_metadata.c.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/no_user_view.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart'; +import 'package:ion/app/features/user/providers/follow_list_provider.c.dart'; + +class FollowingUsers extends ConsumerWidget { + const FollowingUsers({ + required this.onUserSelected, + this.selectedPubkeys, + this.selectable = false, + super.key, + }); + + final void Function(UserMetadataEntity user) onUserSelected; + final List? selectedPubkeys; + final bool selectable; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final followList = ref.watch(currentUserFollowListProvider); + + return followList.maybeWhen( + data: (data) { + final pubkeys = data?.data.list.map((e) => e.pubkey).toList() ?? []; + return ListView.separated( + itemBuilder: (context, index) { + return SelectableUserListItem( + pubkey: pubkeys[index], + onUserSelected: onUserSelected, + selectedPubkeys: selectedPubkeys, + selectable: selectable, + ); + }, + itemCount: pubkeys.length, + separatorBuilder: (context, index) => SizedBox(height: 14.0.s), + ); + }, + orElse: () => const NoUserView(), + ); + } +} diff --git a/lib/app/features/user/pages/user_picker_sheet/components/no_user_view.dart b/lib/app/features/user/pages/user_picker_sheet/components/no_user_view.dart new file mode 100644 index 000000000..96f78a5b6 --- /dev/null +++ b/lib/app/features/user/pages/user_picker_sheet/components/no_user_view.dart @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/generated/assets.gen.dart'; + +class NoUserView extends StatelessWidget { + const NoUserView({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: ScreenSideOffset.small( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + Assets.svg.walletChatNewchat.icon( + size: 48.0.s, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0.s, horizontal: 78.0.s), + child: Text( + context.i18n.new_chat_modal_description, + style: context.theme.appTextThemes.caption2.copyWith( + color: context.theme.appColors.onTertararyBackground, + ), + textAlign: TextAlign.center, + ), + ), + const Spacer( + flex: 2, + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/features/chat/components/searched_user_list/searched_user_list.dart b/lib/app/features/user/pages/user_picker_sheet/components/searched_users.dart similarity index 64% rename from lib/app/features/chat/components/searched_user_list/searched_user_list.dart rename to lib/app/features/user/pages/user_picker_sheet/components/searched_users.dart index fcbcb0d2f..2788537fc 100644 --- a/lib/app/features/chat/components/searched_user_list/searched_user_list.dart +++ b/lib/app/features/user/pages/user_picker_sheet/components/searched_users.dart @@ -2,20 +2,28 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:ion/app/components/list_item/list_item.dart'; import 'package:ion/app/components/scroll_view/load_more_builder.dart'; import 'package:ion/app/extensions/extensions.dart'; -import 'package:ion/app/features/chat/providers/users_data_source_provider.c.dart'; import 'package:ion/app/features/nostr/providers/entities_paged_data_provider.c.dart'; import 'package:ion/app/features/user/model/user_metadata.c.dart'; -import 'package:ion/app/utils/username.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart'; +import 'package:ion/app/features/user/providers/search_users_data_source_provider.c.dart'; -class SearchedUsersList extends ConsumerWidget { - const SearchedUsersList({super.key}); +class SearchedUsers extends ConsumerWidget { + const SearchedUsers({ + required this.onUserSelected, + this.selectable = false, + this.selectedPubkeys, + super.key, + }); + + final void Function(UserMetadataEntity user) onUserSelected; + final bool selectable; + final List? selectedPubkeys; @override Widget build(BuildContext context, WidgetRef ref) { - final dataSource = ref.watch(usersDataSourceProvider).valueOrNull; + final dataSource = ref.watch(searchUsersDataSourceProvider).valueOrNull; final entitiesPagedData = ref.watch(entitiesPagedDataProvider(dataSource)); final users = entitiesPagedData?.data.items?.whereType().toList(); final slivers = [ @@ -27,10 +35,11 @@ class SearchedUsersList extends ConsumerWidget { itemCount: users.length, itemBuilder: (BuildContext context, int index) { final user = users.elementAt(index); - return ListItem.user( - title: Text(user.data.displayName), - subtitle: Text(prefixUsername(username: user.data.name, context: context)), - profilePicture: user.data.picture, + return SelectableUserListItem( + pubkey: user.masterPubkey, + onUserSelected: onUserSelected, + selectedPubkeys: selectedPubkeys, + selectable: selectable, ); }, ), diff --git a/lib/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart b/lib/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart new file mode 100644 index 000000000..10a7d52ee --- /dev/null +++ b/lib/app/features/user/pages/user_picker_sheet/components/selectable_user_list_item.dart @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/components/list_item/list_item.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/user/model/user_metadata.c.dart'; +import 'package:ion/app/features/user/providers/user_metadata_provider.c.dart'; +import 'package:ion/app/utils/username.dart'; +import 'package:ion/generated/assets.gen.dart'; + +class SelectableUserListItem extends ConsumerWidget { + const SelectableUserListItem({ + required this.pubkey, + required this.onUserSelected, + super.key, + this.selectedPubkeys, + this.selectable = false, + }); + + final String pubkey; + final void Function(UserMetadataEntity user) onUserSelected; + final List? selectedPubkeys; + final bool selectable; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userMetadataResult = ref.watch(userMetadataProvider(pubkey)); + + return userMetadataResult.maybeWhen( + data: (user) { + if (user == null) return const SizedBox.shrink(); + + final isSelected = selectedPubkeys?.contains(pubkey) ?? false; + return ListItem.user( + onTap: () => onUserSelected(user), + title: Text(user.data.displayName), + subtitle: Text(prefixUsername(username: user.data.name, context: context)), + profilePicture: user.data.picture, + trailing: selectable + ? isSelected + ? Assets.svg.iconBlockCheckboxOnblue.icon( + color: context.theme.appColors.success, + ) + : Assets.svg.iconBlockCheckboxOff.icon( + color: context.theme.appColors.tertararyText, + ) + : null, + ); + }, + orElse: SizedBox.shrink, + ); + } +} diff --git a/lib/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart b/lib/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart new file mode 100644 index 000000000..460c40d1b --- /dev/null +++ b/lib/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:ion/app/components/inputs/search_input/search_input.dart'; +import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/user/model/user_metadata.c.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/follower_users.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/following_users.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/components/searched_users.dart'; +import 'package:ion/app/features/user/providers/search_users_data_source_provider.c.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; + +enum UserListType { + follower, + following, +} + +class UserPickerSheet extends HookConsumerWidget { + const UserPickerSheet({ + required this.navigationBar, + required this.onUserSelected, + this.header, + super.key, + this.selectedPubkeys, + this.selectable = false, + this.bottomContent, + this.initialUserListType = UserListType.following, + }); + + final NavigationAppBar navigationBar; + final Widget? header; + final List? selectedPubkeys; + final bool selectable; + final Widget? bottomContent; + final void Function(UserMetadataEntity user) onUserSelected; + final UserListType initialUserListType; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final searchText = ref.watch(searchUsersQueryProvider); + + return Column( + children: [ + navigationBar, + Expanded( + child: ScreenSideOffset.small( + child: Column( + children: [ + SearchInput( + textInputAction: TextInputAction.search, + onTextChanged: (text) { + ref.read(searchUsersQueryProvider.notifier).text = text; + }, + ), + SizedBox(height: 12.0.s), + if (header != null) + Column( + children: [ + header!, + SizedBox(height: 12.0.s), + ], + ), + Expanded( + child: searchText.isEmpty + ? initialUserListType == UserListType.follower + ? FollowerUsers( + onUserSelected: onUserSelected, + selectedPubkeys: selectedPubkeys, + selectable: selectable, + ) + : FollowingUsers( + onUserSelected: onUserSelected, + selectedPubkeys: selectedPubkeys, + selectable: selectable, + ) + : SearchedUsers( + onUserSelected: onUserSelected, + selectedPubkeys: selectedPubkeys, + selectable: selectable, + ), + ), + bottomContent ?? const SizedBox.shrink(), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/app/features/chat/providers/users_data_source_provider.c.dart b/lib/app/features/user/providers/search_users_data_source_provider.c.dart similarity index 80% rename from lib/app/features/chat/providers/users_data_source_provider.c.dart rename to lib/app/features/user/providers/search_users_data_source_provider.c.dart index e2d99862d..79e0ec868 100644 --- a/lib/app/features/chat/providers/users_data_source_provider.c.dart +++ b/lib/app/features/user/providers/search_users_data_source_provider.c.dart @@ -8,11 +8,11 @@ import 'package:ion/app/features/user/model/user_metadata.c.dart'; import 'package:nostr_dart/nostr_dart.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'users_data_source_provider.c.g.dart'; +part 'search_users_data_source_provider.c.g.dart'; @riverpod -Future> usersDataSource(Ref ref) async { - final searchText = ref.watch(usersSearchTextProvider); +Future> searchUsersDataSource(Ref ref) async { + final searchText = ref.watch(searchUsersQueryProvider); await ref.debounce(); return [ @@ -31,7 +31,7 @@ Future> usersDataSource(Ref ref) async { } @riverpod -class UsersSearchText extends _$UsersSearchText { +class SearchUsersQuery extends _$SearchUsersQuery { @override String build() { return ''; diff --git a/lib/app/features/wallet/views/pages/coins_flow/send_coins/components/send_coins_form.dart b/lib/app/features/wallet/views/pages/coins_flow/send_coins/components/send_coins_form.dart index eeb03cf80..cde0fbd5d 100644 --- a/lib/app/features/wallet/views/pages/coins_flow/send_coins/components/send_coins_form.dart +++ b/lib/app/features/wallet/views/pages/coins_flow/send_coins/components/send_coins_form.dart @@ -78,12 +78,10 @@ class SendCoinsForm extends HookConsumerWidget { contactId: selectedContact?.id, onClearTap: (contactId) => notifier.setContact(null), onContactTap: () async { - final contactId = await ContactsListRoute( - title: context.i18n.contacts_select_title, - ).push(context); + final pubkey = await CoinsSelectFriendRoute().push(context); - if (contactId != null) { - final contact = ref.read(contactByIdProvider(id: contactId)); + if (pubkey != null) { + final contact = ref.read(contactByIdProvider(id: pubkey)); notifier.setContact(contact); } }, diff --git a/lib/app/features/wallet/views/pages/friends_modal/friends_modal.dart b/lib/app/features/wallet/views/pages/friends_modal/friends_modal.dart new file mode 100644 index 000000000..1f150d817 --- /dev/null +++ b/lib/app/features/wallet/views/pages/friends_modal/friends_modal.dart @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: ice License 1.0 + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:ion/app/extensions/extensions.dart'; +import 'package:ion/app/features/user/pages/user_picker_sheet/user_picker_sheet.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_app_bar.dart'; +import 'package:ion/app/router/components/navigation_app_bar/navigation_close_button.dart'; +import 'package:ion/app/router/components/sheet_content/sheet_content.dart'; + +class FriendsModal extends StatelessWidget { + const FriendsModal({super.key}); + + @override + Widget build(BuildContext context) { + return SheetContent( + topPadding: 0, + body: UserPickerSheet( + navigationBar: NavigationAppBar.modal( + title: Text(context.i18n.friends_modal_title), + actions: const [NavigationCloseButton()], + ), + onUserSelected: (user) => context.pop(user.masterPubkey), + ), + ); + } +} diff --git a/lib/app/features/wallet/views/pages/send_nft_form/send_nft_form.dart b/lib/app/features/wallet/views/pages/send_nft_form/send_nft_form.dart index 99bdd5afb..744f31aeb 100644 --- a/lib/app/features/wallet/views/pages/send_nft_form/send_nft_form.dart +++ b/lib/app/features/wallet/views/pages/send_nft_form/send_nft_form.dart @@ -68,12 +68,10 @@ class SendNftForm extends ConsumerWidget { notifier.setContact(null), }, onContactTap: () async { - final contactId = await NftContactsListRoute( - title: context.i18n.contacts_select_title, - ).push(context); + final pubkey = await NftSelectFriendRoute().push(context); - if (contactId != null) { - final contact = ref.read(contactByIdProvider(id: contactId)); + if (pubkey != null) { + final contact = ref.read(contactByIdProvider(id: pubkey)); notifier.setContact(contact); } }, diff --git a/lib/app/features/wallet/views/pages/wallet_page/components/contacts/contacts_list_header.dart b/lib/app/features/wallet/views/pages/wallet_page/components/contacts/contacts_list_header.dart index 17745b781..5c6f74267 100644 --- a/lib/app/features/wallet/views/pages/wallet_page/components/contacts/contacts_list_header.dart +++ b/lib/app/features/wallet/views/pages/wallet_page/components/contacts/contacts_list_header.dart @@ -1,13 +1,13 @@ // SPDX-License-Identifier: ice License 1.0 +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:ion/app/components/screen_offset/screen_side_offset.dart'; import 'package:ion/app/constants/ui.dart'; import 'package:ion/app/extensions/build_context.dart'; import 'package:ion/app/extensions/num.dart'; import 'package:ion/app/extensions/theme_data.dart'; -import 'package:ion/app/features/contacts/pages/contacts_list_view.dart'; -import 'package:ion/app/features/wallet/model/contact_data.c.dart'; import 'package:ion/app/router/app_routes.c.dart'; class ContactListHeader extends StatelessWidget { @@ -32,10 +32,14 @@ class ContactListHeader extends StatelessWidget { ), ), TextButton( - onPressed: () => ContactsListRoute( - title: context.i18n.contacts_title, - action: ContactRouteAction.navigate, - ).push(context), + onPressed: () async { + final pubkey = await NftSelectFriendRoute().push(context); + if (pubkey != null) { + if (context.mounted) { + unawaited(ContactRoute(contactId: pubkey).push(context)); + } + } + }, child: Padding( padding: EdgeInsets.all(UiConstants.hitSlop), child: Text( diff --git a/lib/app/router/app_routes.c.dart b/lib/app/router/app_routes.c.dart index c8db88155..dec04fcda 100644 --- a/lib/app/router/app_routes.c.dart +++ b/lib/app/router/app_routes.c.dart @@ -28,7 +28,7 @@ import 'package:ion/app/features/chat/views/pages/new_channel_modal/new_channel_ import 'package:ion/app/features/chat/views/pages/new_chat_modal/new_chat_modal.dart'; import 'package:ion/app/features/chat/views/pages/new_group_modal/pages/add_group_participants_modal.dart'; import 'package:ion/app/features/chat/views/pages/new_group_modal/pages/create_group_modal.dart'; -import 'package:ion/app/features/contacts/pages/contacts_list_view.dart'; +import 'package:ion/app/features/chat/views/pages/share_profile_modal/share_profile_modal.dart'; import 'package:ion/app/features/core/model/language.dart'; import 'package:ion/app/features/core/views/pages/app_test_page/app_test_page.dart'; import 'package:ion/app/features/core/views/pages/error_page.dart'; @@ -123,6 +123,7 @@ import 'package:ion/app/features/wallet/views/pages/coins_flow/send_coins/compon import 'package:ion/app/features/wallet/views/pages/coins_flow/send_coins/components/send_coins_form.dart'; import 'package:ion/app/features/wallet/views/pages/coins_flow/send_coins/send_coin_modal_page.dart'; import 'package:ion/app/features/wallet/views/pages/contact_modal_page/contact_modal_page.dart'; +import 'package:ion/app/features/wallet/views/pages/friends_modal/friends_modal.dart'; import 'package:ion/app/features/wallet/views/pages/manage_coins/manage_coins_page.dart'; import 'package:ion/app/features/wallet/views/pages/manage_nfts/manage_nfts_page.dart'; import 'package:ion/app/features/wallet/views/pages/nft_details/nft_details_page.dart'; diff --git a/lib/app/router/chat_routes.dart b/lib/app/router/chat_routes.dart index d33c5614b..54cfdde65 100644 --- a/lib/app/router/chat_routes.dart +++ b/lib/app/router/chat_routes.dart @@ -82,17 +82,11 @@ class ChatLearnMoreModalRoute extends BaseRouteData { } class ShareProfileModalRoute extends BaseRouteData { - ShareProfileModalRoute({required this.title, this.action = ContactRouteAction.pop}) + ShareProfileModalRoute() : super( - child: ContactsListView( - appBarTitle: title, - action: action, - ), + child: const ShareProfileModal(), type: IceRouteType.bottomSheet, ); - - final String title; - final ContactRouteAction action; } class ChatAddPollModalRoute extends BaseRouteData { diff --git a/lib/app/router/wallet_routes.dart b/lib/app/router/wallet_routes.dart index d1fa82601..2b9bc880e 100644 --- a/lib/app/router/wallet_routes.dart +++ b/lib/app/router/wallet_routes.dart @@ -46,7 +46,7 @@ class WalletRoutes { TypedGoRoute( path: 'nft-send', routes: [ - TypedGoRoute(path: 'nft-contacts-list'), + TypedGoRoute(path: 'select-friend'), ], ), TypedGoRoute(path: 'nft-confirm'), @@ -65,7 +65,7 @@ class WalletRoutes { TypedGoRoute( path: 'coin-send-form', routes: [ - TypedGoRoute(path: 'contacts-list'), + TypedGoRoute(path: 'select-friend'), ], ), TypedGoRoute(path: 'scan-receiver-wallet'), @@ -209,33 +209,20 @@ class CoinsSendFormRoute extends BaseRouteData { ); } -class ContactsListRoute extends BaseRouteData { - ContactsListRoute({required this.title, this.action = ContactRouteAction.pop}) +class CoinsSelectFriendRoute extends BaseRouteData { + CoinsSelectFriendRoute() : super( - child: ContactsListView( - appBarTitle: title, - action: action, - ), + child: const FriendsModal(), type: IceRouteType.bottomSheet, ); - - final String title; - final ContactRouteAction action; } -class NftContactsListRoute extends BaseRouteData { - NftContactsListRoute({required this.title, this.action = ContactRouteAction.pop}) +class NftSelectFriendRoute extends BaseRouteData { + NftSelectFriendRoute() : super( - child: ContactsListView( - action: action, - appBarTitle: title, - showBackButton: true, - ), + child: const FriendsModal(), type: IceRouteType.bottomSheet, ); - - final String title; - final ContactRouteAction action; } class CoinsSendFormConfirmationRoute extends BaseRouteData { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 24a3e5641..79a93230d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -366,6 +366,7 @@ "sorting_price_desc": "Price: high to low", "wallet_sorting_title": "Sorting the list", "contacts_title": "Contacts", + "friends_modal_title": "Select friend", "contacts_select_title": "Select contact", "contacts_allow_pop_up_title": "Ice is better with friends", "contacts_allow_pop_up_desc": "Sync your contacts, see who is already on Ice, and send and receive Ice payments from any of your contacts.", @@ -578,7 +579,7 @@ "chat_modal_group_description": "Chat with multiple people together", "chat_modal_channel_description": "Share updates with a wide audience", "chat_profile_share_modal_title": "Share profile", - "new_chat_modal_title": "New chat", + "new_chat_modal_title": "New Chat", "new_chat_modal_description": "Search above for users, groups, and channels...", "new_chat_modal_new_group_button": "New group", "new_chat_modal_new_channel_button": "New channel", @@ -602,7 +603,7 @@ "channel_create_type_public_desc": "Public channels are searchable and open to all users.", "channel_create_type_private_desc": "Private channels can't be found in search and aren't end-to-end encrypted.", "channel_create_admins_title": "Admin management", - "channel_create_admins_action": "Add administrator", + "channel_create_admins_action": "Add Administrator", "channel_create_admin_type_title": "Choose admin type", "channel_create_admin_type_owner": "Owner", "channel_create_admin_type_admin": "Admin",