diff --git a/CHANGELOG.md b/CHANGELOG.md index f17a15ed..180a2e99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.2.17 - New: Adds an "Ask a Friend" button to new contact suggestions. +- Improved: Onboarding flow for new users. - Improved: The blue verification checkmark now displays the total number of verifications. - Fix: Issue with receiving messages when user closed app while decrypting - Fix: Background message fetching reliability. diff --git a/lib/app.dart b/lib/app.dart index a43651dd..fd6476c3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -137,12 +137,14 @@ class _AppMainWidgetState extends State { bool _isLoaded = false; bool _isTwonlyLocked = true; bool _wasLogged = true; + late int _initialPage; (Future?, bool) _proofOfWork = (null, false); @override void initState() { super.initState(); + _initialPage = widget.initialPage; Log.info('AppWidgetState: initState started'); initAsync(); } @@ -150,6 +152,12 @@ class _AppMainWidgetState extends State { Future initAsync() async { Log.info('AppWidgetState: initAsync started'); if (userService.isUserCreated) { + if (_initialPage != 0) { + final count = await twonlyDB.contactsDao.getContactsCount(); + if (count == 0) { + _initialPage = 0; + } + } try { unawaited(FirebaseMessaging.instance.requestPermission()); } catch (e) { @@ -200,8 +208,7 @@ class _AppMainWidgetState extends State { _isTwonlyLocked = false; }), ); - } else if (!userService.currentUser.skipSetupPages && - userService.currentUser.currentSetupPage != null) { + } else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) { // This will only be shown in case the user have not skipped child = SetupView( onUpdate: () => setState(() { @@ -210,7 +217,7 @@ class _AppMainWidgetState extends State { ); } else { child = HomeView( - initialPage: widget.initialPage, + initialPage: _initialPage, ); } } else if (_showOnboarding) { diff --git a/lib/src/database/daos/contacts.dao.dart b/lib/src/database/daos/contacts.dao.dart index 9258e637..64ce50f3 100644 --- a/lib/src/database/daos/contacts.dao.dart +++ b/lib/src/database/daos/contacts.dao.dart @@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor with _$ContactsDaoMixin { return select(contacts).get(); } + Future getContactsCount() async { + final count = contacts.userId.count(); + final query = selectOnly(contacts)..addColumns([count]); + final result = await query.map((row) => row.read(count)).getSingle(); + return result ?? 0; + } + Stream watchContactsBlocked() { final count = contacts.userId.count(); final query = selectOnly(contacts) diff --git a/lib/src/visual/components/contact_request_badge.comp.dart b/lib/src/visual/components/contact_request_badge.comp.dart new file mode 100644 index 00000000..4a08c5b1 --- /dev/null +++ b/lib/src/visual/components/contact_request_badge.comp.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/notification_badge.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +class ContactRequestBadgeComp extends StatelessWidget { + const ContactRequestBadgeComp({super.key}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: twonlyDB.contactsDao.watchContactsRequestedCount(), + builder: (context, snapshot) { + final count = snapshot.data ?? 0; + if (count == 0) { + return const SizedBox.shrink(); + } + return Stack( + children: [ + Positioned.fill( + child: Center( + child: Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: primaryColor, + shape: BoxShape.circle, + ), + ), + ), + ), + Center( + child: NotificationBadgeComp( + backgroundColor: isDarkMode(context) ? Colors.white : Colors.black, + textColor: isDarkMode(context) ? Colors.black : Colors.white, + count: count.toString(), + child: IconButton( + color: Colors.black, + icon: const FaIcon( + FontAwesomeIcons.userPlus, + size: 18, + ), + onPressed: () => context.push(Routes.chatsAddNewUser), + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/src/visual/components/profile_qr_code.comp.dart b/lib/src/visual/components/profile_qr_code.comp.dart index bc917f46..86c05c7a 100644 --- a/lib/src/visual/components/profile_qr_code.comp.dart +++ b/lib/src/visual/components/profile_qr_code.comp.dart @@ -44,53 +44,52 @@ class _ProfileQrCodeCompState extends State { @override Widget build(BuildContext context) { - if (_isLoading || _qrCode == null) { - return SizedBox( - width: widget.size, - height: widget.size, - child: const Center( - child: CircularProgressIndicator(), - ), - ); - } - - return Container( - // padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - color: context.color.primary, - borderRadius: BorderRadius.circular(12), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 3, - offset: Offset(0, 2), - ), - ], - ), - child: QrImageView.withQr( - qr: QrCode.fromData( - data: _qrCode!, - errorCorrectLevel: QrErrorCorrectLevel.M, - ), - eyeStyle: QrEyeStyle( - color: isDarkMode(context) ? Colors.black : Colors.white, - borderRadius: 2, - ), - dataModuleStyle: QrDataModuleStyle( - color: isDarkMode(context) ? Colors.black : Colors.white, - borderRadius: 2, - ), - gapless: false, - embeddedImage: (widget.showAvatar && _userAvatar != null) - ? MemoryImage(_userAvatar!) - : null, - embeddedImageStyle: QrEmbeddedImageStyle( - size: const Size(60, 66), - embeddedImageShape: EmbeddedImageShape.square, - shapeColor: context.color.primary, - safeArea: true, - ), - size: widget.size, + final loaded = !_isLoading && _qrCode != null; + return SizedBox( + width: widget.size, + height: widget.size, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: loaded + ? Container( + key: const ValueKey('qr_code_container'), + // padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: context.color.primary, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 3, + offset: Offset(0, 2), + ), + ], + ), + child: QrImageView.withQr( + qr: QrCode.fromData( + data: _qrCode!, + errorCorrectLevel: QrErrorCorrectLevel.M, + ), + eyeStyle: QrEyeStyle( + color: isDarkMode(context) ? Colors.black : Colors.white, + borderRadius: 2, + ), + dataModuleStyle: QrDataModuleStyle( + color: isDarkMode(context) ? Colors.black : Colors.white, + borderRadius: 2, + ), + gapless: false, + embeddedImage: (widget.showAvatar && _userAvatar != null) ? MemoryImage(_userAvatar!) : null, + embeddedImageStyle: QrEmbeddedImageStyle( + size: const Size(60, 66), + embeddedImageShape: EmbeddedImageShape.square, + shapeColor: context.color.primary, + safeArea: true, + ), + size: widget.size, + ), + ) + : const SizedBox.shrink(key: ValueKey('qr_code_placeholder')), ), ); } diff --git a/lib/src/visual/views/camera/share_image_contact_selection.view.dart b/lib/src/visual/views/camera/share_image_contact_selection.view.dart index 318fd5a6..c968a3c0 100644 --- a/lib/src/visual/views/camera/share_image_contact_selection.view.dart +++ b/lib/src/visual/views/camera/share_image_contact_selection.view.dart @@ -3,9 +3,7 @@ import 'dart:collection'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:go_router/go_router.dart'; import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -15,6 +13,7 @@ import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; +import 'package:twonly/src/visual/components/contact_request_badge.comp.dart'; import 'package:twonly/src/visual/components/flame_counter.comp.dart'; import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/elements/headline.element.dart'; @@ -22,6 +21,7 @@ import 'package:twonly/src/visual/helpers/screenshot.helper.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/best_friends_selector.dart'; import 'package:twonly/src/visual/views/camera/share_image_contact_selection_components/shortcut_row.comp.dart'; import 'package:twonly/src/visual/views/camera/share_image_editor_components/layers/background.layer.dart'; +import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ @@ -111,9 +111,7 @@ class _ShareImageView extends State { for (final group in groups) { if (group.pinned) continue; - if (!group.archived && - getFlameCounterFromGroup(group).counter > 0 && - bestFriends.length < 6) { + if (!group.archived && getFlameCounterFromGroup(group).counter > 0 && bestFriends.length < 6) { bestFriends.add(group); } else { otherUsers.add(group); @@ -133,10 +131,7 @@ class _ShareImageView extends State { await updateGroups( _allGroups .where( - (x) => - !x.archived || - !hideArchivedUsers || - widget.selectedGroupIds.contains(x.groupId), + (x) => !x.archived || !hideArchivedUsers || widget.selectedGroupIds.contains(x.groupId), ) .toList(), ); @@ -160,31 +155,23 @@ class _ShareImageView extends State { return Scaffold( appBar: AppBar( title: Text(context.lang.shareImageTitle), + actions: const [ + ContactRequestBadgeComp(), + SizedBox(width: 15), + ], ), body: SafeArea( child: Padding( padding: const EdgeInsets.only( bottom: 40, left: 10, - top: 20, right: 10, ), - child: Column( + child: ListView( children: [ if (_allGroups.isEmpty) - Expanded( - child: Center( - child: FilledButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () => context.push(Routes.chatsAddNewUser), - label: Text( - context.lang.chatListViewSearchUserNameBtn, - ), - ), - ), - ), - - if (_allGroups.isNotEmpty) + const EmptyChatListComp() + else ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: TextField( @@ -195,163 +182,161 @@ class _ShareImageView extends State { ), ), ), - const SizedBox(height: 10), - ShortcutRowComp( - selectedGroupIds: widget.selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - ), - if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), - BestFriendsSelector( - groups: _pinnedContacts, - selectedGroupIds: widget.selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - title: context.lang.shareImagePinnedContacts, - showSelectAll: - !widget.mediaFileService.mediaFile.requiresAuthentication, - ), - const SizedBox(height: 10), - BestFriendsSelector( - groups: _bestFriends, - selectedGroupIds: widget.selectedGroupIds, - updateSelectedGroupIds: updateSelectedGroupIds, - title: context.lang.shareImageBestFriends, - showSelectAll: - !widget.mediaFileService.mediaFile.requiresAuthentication, - ), - const SizedBox(height: 10), - if (_otherUsers.isNotEmpty) - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - HeadLineComp(context.lang.shareImageAllUsers), - if (_allGroups.any((x) => x.archived)) - Row( - children: [ - Text( - context.lang.shareImageShowArchived, - style: const TextStyle(fontSize: 10), - ), - Transform.scale( - scale: 0.75, - child: Checkbox( - value: !hideArchivedUsers, - side: WidgetStateBorderSide.resolveWith( - (states) { - if (states.contains(WidgetState.selected)) { - return const BorderSide(width: 0); - } - return BorderSide( - color: Theme.of( - context, - ).colorScheme.outline, - ); + const SizedBox(height: 10), + ShortcutRowComp( + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + ), + if (_pinnedContacts.isNotEmpty) const SizedBox(height: 10), + BestFriendsSelector( + groups: _pinnedContacts, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + title: context.lang.shareImagePinnedContacts, + showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication, + ), + const SizedBox(height: 10), + BestFriendsSelector( + groups: _bestFriends, + selectedGroupIds: widget.selectedGroupIds, + updateSelectedGroupIds: updateSelectedGroupIds, + title: context.lang.shareImageBestFriends, + showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication, + ), + const SizedBox(height: 10), + if (_otherUsers.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + HeadLineComp(context.lang.shareImageAllUsers), + if (_allGroups.any((x) => x.archived)) + Row( + children: [ + Text( + context.lang.shareImageShowArchived, + style: const TextStyle(fontSize: 10), + ), + Transform.scale( + scale: 0.75, + child: Checkbox( + value: !hideArchivedUsers, + side: WidgetStateBorderSide.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return const BorderSide(width: 0); + } + return BorderSide( + color: Theme.of( + context, + ).colorScheme.outline, + ); + }, + ), + onChanged: (a) async { + hideArchivedUsers = !hideArchivedUsers; + await _filterUsers(lastQuery); + if (mounted) setState(() {}); }, ), - onChanged: (a) async { - hideArchivedUsers = !hideArchivedUsers; - await _filterUsers(lastQuery); - if (mounted) setState(() {}); - }, ), - ), - ], - ), - ], - ), - if (_otherUsers.isNotEmpty) - Expanded( - child: UserList( + ], + ), + ], + ), + if (_otherUsers.isNotEmpty) + UserList( List.from(_otherUsers), selectedGroupIds: widget.selectedGroupIds, updateSelectedGroupIds: updateSelectedGroupIds, ), - ), + ], ], ), ), ), - floatingActionButton: SizedBox( - height: 168, - child: Padding( - padding: const EdgeInsets.only(bottom: 20, right: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (widget.mediaFileService.mediaFile.type == MediaType.image && - _screenshotImage?.image != null && - userService.currentUser.showShowImagePreviewWhenSending) - SizedBox( - height: 100, - width: 100 * 9 / 16, - child: Container( - clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - border: Border.all( - color: context.color.primary, - width: 2, - ), - color: context.color.primary, - borderRadius: BorderRadius.circular(12), - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: CustomPaint( - painter: UiImagePainter(_screenshotImage!.image!), - ), - ), - ), - ), - FilledButton.icon( - icon: !mediaStoreFutureReady || sendingImage - ? SizedBox( - height: 12, - width: 12, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.inversePrimary, + floatingActionButton: _allGroups.isEmpty + ? null + : SizedBox( + height: 168, + child: Padding( + padding: const EdgeInsets.only(bottom: 20, right: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (widget.mediaFileService.mediaFile.type == MediaType.image && + _screenshotImage?.image != null && + userService.currentUser.showShowImagePreviewWhenSending) + SizedBox( + height: 100, + width: 100 * 9 / 16, + child: Container( + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + border: Border.all( + color: context.color.primary, + width: 2, + ), + color: context.color.primary, + borderRadius: BorderRadius.circular(12), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: CustomPaint( + painter: UiImagePainter(_screenshotImage!.image!), + ), + ), ), - ) - : const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () async { - if (!mediaStoreFutureReady || - widget.selectedGroupIds.isEmpty) { - return; - } + ), + FilledButton.icon( + icon: !mediaStoreFutureReady || sendingImage + ? SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.inversePrimary, + ), + ) + : const FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () async { + if (!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty) { + return; + } - setState(() { - sendingImage = true; - }); + setState(() { + sendingImage = true; + }); - // in case mediaStoreFutureReady is ready, the image is stored in the originalPath - await insertMediaFileInMessagesTable( - widget.mediaFileService, - widget.selectedGroupIds.toList(), - additionalData: widget.additionalData, - ); + // in case mediaStoreFutureReady is ready, the image is stored in the originalPath + await insertMediaFileInMessagesTable( + widget.mediaFileService, + widget.selectedGroupIds.toList(), + additionalData: widget.additionalData, + ); - if (context.mounted) { - Navigator.pop(context, true); - } - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric(vertical: 10, horizontal: 30), - ), - backgroundColor: WidgetStateProperty.all( - !mediaStoreFutureReady || widget.selectedGroupIds.isEmpty - ? context.color.onSurface - : context.color.primary, - ), - ), - label: Text( - '${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})', - style: const TextStyle(fontSize: 17), + if (context.mounted) { + Navigator.pop(context, true); + } + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(vertical: 10, horizontal: 30), + ), + backgroundColor: WidgetStateProperty.all( + !mediaStoreFutureReady || widget.selectedGroupIds.isEmpty + ? context.color.onSurface + : context.color.primary, + ), + ), + label: Text( + '${context.lang.shareImagedEditorSendImage} (${widget.selectedGroupIds.length})', + style: const TextStyle(fontSize: 17), + ), + ), + ], ), ), - ], - ), - ), - ), + ), ); } } @@ -375,6 +360,8 @@ class UserList extends StatelessWidget { ); return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), restorationId: 'new_message_users_list', itemCount: groups.length, itemBuilder: (context, i) { diff --git a/lib/src/visual/views/chats/chat_list.view.dart b/lib/src/visual/views/chats/chat_list.view.dart index 8a889073..4e1979e1 100644 --- a/lib/src/visual/views/chats/chat_list.view.dart +++ b/lib/src/visual/views/chats/chat_list.view.dart @@ -18,6 +18,7 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/connection_status.comp.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/themes/light.dart'; +import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.comp.dart'; @@ -31,11 +32,15 @@ class ChatListView extends StatefulWidget { class _ChatListViewState extends State { StreamSubscription? _userSub; - late StreamSubscription> _contactsSub; + StreamSubscription>? _contactsSub; + StreamSubscription>? _contactsCountSub; List _groupsNotPinned = []; List _groupsPinned = []; List _groupsArchived = []; + bool _hasContacts = true; + bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || _groupsArchived.isNotEmpty || _groupsPinned.isNotEmpty; + GlobalKey searchForOtherUsers = GlobalKey(); bool showFeedbackShortcut = false; @@ -58,33 +63,34 @@ class _ChatListViewState extends State { _contactsSub = stream.listen((groups) { if (!mounted) return; setState(() { - _groupsNotPinned = groups - .where((x) => !x.pinned && !x.archived) - .toList(); + _groupsNotPinned = groups.where((x) => !x.pinned && !x.archived).toList(); _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); _groupsArchived = groups.where((x) => x.archived).toList(); }); }); - _countContactRequestStream = twonlyDB.contactsDao - .watchContactsRequestedCount() - .listen((update) { - if (update != null) { - if (!mounted) return; - setState(() { - _countContactRequest = update; - }); - } - }); + _contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((contacts) { + if (!mounted) return; + setState(() { + _hasContacts = contacts.isNotEmpty; + }); + }); - _countAnnouncedStream = twonlyDB.userDiscoveryDao - .watchNewAnnouncementsWithDataCount() - .listen((update) { - if (!mounted) return; - setState(() { - _countAnnouncedUsers = update; - }); + _countContactRequestStream = twonlyDB.contactsDao.watchContactsRequestedCount().listen((update) { + if (update != null) { + if (!mounted) return; + setState(() { + _countContactRequest = update; }); + } + }); + + _countAnnouncedStream = twonlyDB.userDiscoveryDao.watchNewAnnouncementsWithDataCount().listen((update) { + if (!mounted) return; + setState(() { + _countAnnouncedUsers = update; + }); + }); WidgetsBinding.instance.addPostFrameCallback((_) async { final changeLog = await rootBundle.loadString('CHANGELOG.md'); @@ -93,8 +99,7 @@ class _ChatListViewState extends State { changeLog.codeUnits, )).bytes; if (!userService.currentUser.hideChangeLog && - userService.currentUser.lastChangeLogHash.toString() != - changeLogHash.toString()) { + userService.currentUser.lastChangeLogHash.toString() != changeLogHash.toString()) { await UserService.update((u) { u.lastChangeLogHash = changeLogHash; }); @@ -113,7 +118,8 @@ class _ChatListViewState extends State { @override void dispose() { - _contactsSub.cancel(); + _contactsSub?.cancel(); + _contactsCountSub?.cancel(); _countContactRequestStream.cancel(); _countAnnouncedStream.cancel(); _userSub?.cancel(); @@ -182,16 +188,11 @@ class _ChatListViewState extends State { ), Center( child: NotificationBadgeComp( - backgroundColor: isDarkMode(context) - ? Colors.white - : Colors.black, + backgroundColor: isDarkMode(context) ? Colors.white : Colors.black, textColor: isDarkMode(context) ? Colors.black : Colors.white, - count: (_countAnnouncedUsers + _countContactRequest) - .toString(), + count: (_countAnnouncedUsers + _countContactRequest).toString(), child: IconButton( - color: (_countAnnouncedUsers + _countContactRequest > 0) - ? Colors.black - : null, + color: (_countAnnouncedUsers + _countContactRequest > 0) ? Colors.black : null, key: searchForOtherUsers, icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), onPressed: () => context.push(Routes.chatsAddNewUser), @@ -217,21 +218,11 @@ class _ChatListViewState extends State { children: [ const FinishSetupComp(), const MissingBackupComp(), - if (_groupsNotPinned.isEmpty && - _groupsPinned.isEmpty && - _groupsArchived.isEmpty) + if (!_hasOpenGroup) Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: FilledButton.icon( - icon: const Icon(Icons.person_add), - onPressed: () => context.push(Routes.chatsAddNewUser), - label: Text( - context.lang.chatListViewSearchUserNameBtn, - ), - ), - ), + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [EmptyChatListComp()], ), ) else @@ -243,10 +234,7 @@ class _ChatListViewState extends State { _groupsNotPinned.length + (_groupsArchived.isNotEmpty ? 1 : 0), itemBuilder: (context, index) { - if (index >= - _groupsNotPinned.length + - _groupsPinned.length + - (_groupsPinned.isNotEmpty ? 1 : 0)) { + if (index >= _groupsNotPinned.length + _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0)) { if (_groupsArchived.isEmpty) return Container(); return ListTile( title: Text( @@ -289,42 +277,44 @@ class _ChatListViewState extends State { ], ), ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(bottom: 30), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Material( - elevation: 3, - shape: const CircleBorder(), - color: context.color.primary, - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () => context.push(Routes.settingsPublicProfile), - child: SizedBox( - width: 45, - height: 45, - child: Center( + floatingActionButton: !_hasContacts + ? null + : Padding( + padding: const EdgeInsets.only(bottom: 30), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Material( + elevation: 3, + shape: const CircleBorder(), + color: context.color.primary, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () => context.push(Routes.settingsPublicProfile), + child: SizedBox( + width: 45, + height: 45, + child: Center( + child: FaIcon( + FontAwesomeIcons.qrcode, + color: isDarkMode(context) ? Colors.black : Colors.white, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + FloatingActionButton( + backgroundColor: context.color.primary, + onPressed: () => context.push(Routes.chatsStartNewChat), child: FaIcon( - FontAwesomeIcons.qrcode, + FontAwesomeIcons.penToSquare, color: isDarkMode(context) ? Colors.black : Colors.white, ), ), - ), + ], ), ), - const SizedBox(height: 12), - FloatingActionButton( - backgroundColor: context.color.primary, - onPressed: () => context.push(Routes.chatsStartNewChat), - child: FaIcon( - FontAwesomeIcons.penToSquare, - color: isDarkMode(context) ? Colors.black : Colors.white, - ), - ), - ], - ), - ), ); } } diff --git a/lib/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart b/lib/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart new file mode 100644 index 00000000..78118ba0 --- /dev/null +++ b/lib/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart @@ -0,0 +1,119 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart' show FaIcon, FontAwesomeIcons; +import 'package:go_router/go_router.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; +import 'package:twonly/src/services/signal/identity.signal.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; + +class EmptyChatListComp extends StatelessWidget { + const EmptyChatListComp({super.key}); + + Future _shareProfile(BuildContext context) async { + try { + final pubKey = await getUserPublicKey(); + final params = ShareParams( + text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}', + ); + await SharePlus.instance.share(params); + } catch (e) { + if (context.mounted) { + await context.push(Routes.chatsAddNewUser); + } + } + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + const SizedBox( + height: 24, + width: double.infinity, + ), + const Text( + 'Find your first friend', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 28, + ), + ), + const SizedBox(height: 8), + Text( + 'Let friends scan your QR code, or share them your profile.', + style: TextStyle( + fontSize: 14, + color: context.color.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 36), + const Center(child: ProfileQrCodeComp()), + const SizedBox(height: 36), + // 3. Action Buttons + // Button 1: Share Profile (Full Width) + FilledButton.icon( + style: primaryColorButtonStyle, + onPressed: () => _shareProfile(context), + icon: const FaIcon(FontAwesomeIcons.shareNodes, size: 20), + label: const Text( + 'Share your profile', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 12), + // Button Row: Scan QR Code & Enter Username + Row( + children: [ + Expanded( + child: FilledButton.icon( + style: secondaryGreyButtonStyle(context), + onPressed: () => context.push(Routes.cameraQRScanner), + icon: const Icon(Icons.qr_code_scanner_rounded, size: 20), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Scan QR Code', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: FilledButton.icon( + style: secondaryGreyButtonStyle(context), + onPressed: () => context.push(Routes.chatsAddNewUser), + icon: const Icon(Icons.person_add_rounded, size: 20), + label: const FittedBox( + fit: BoxFit.scaleDown, + child: Text( + 'Add by Username', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ], + ), + const SizedBox(height: 50), + ], + ), + ); + } +} diff --git a/lib/src/visual/views/contact/add_new_contact.view.dart b/lib/src/visual/views/contact/add_new_contact.view.dart index 130710a4..5c631673 100644 --- a/lib/src/visual/views/contact/add_new_contact.view.dart +++ b/lib/src/visual/views/contact/add_new_contact.view.dart @@ -62,24 +62,21 @@ class _SearchUsernameView extends State { } }, ); - _newAnnouncedUsersStream = twonlyDB.userDiscoveryDao - .watchNewAnnouncedUsersWithRelations() - .listen((update) { - if (mounted) { - setState(() { - _newAnnouncedUsers = update; - }); - } + + _newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) { + if (mounted) { + setState(() { + _newAnnouncedUsers = update; }); - _allAnnouncedUsersStream = twonlyDB.userDiscoveryDao - .watchAllAnnouncedUsersWithRelations() - .listen((update) { - if (mounted) { - setState(() { - _allAnnouncedUsers = update; - }); - } + } + }); + _allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) { + if (mounted) { + setState(() { + _allAnnouncedUsers = update; }); + } + }); if (widget.username != null) { _usernameController.text = widget.username!; @@ -93,8 +90,7 @@ class _SearchUsernameView extends State { Future _shareProfile() async { final pubKey = await getUserPublicKey(); final params = ShareParams( - text: - 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}', + text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}', ); await SharePlus.instance.share(params); } @@ -194,9 +190,7 @@ class _SearchUsernameView extends State { ), ); - if (widget.publicKey != null && - mounted && - widget.publicKey!.equals(userdata.publicIdentityKey)) { + if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) { final markAsVerified = await showAlertDialog( context, context.lang.linkFromUsername(username), @@ -321,15 +315,9 @@ class _SearchUsernameView extends State { FontAwesomeIcons.shareNodes, size: 14, ), - label: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - context.lang.shareYourProfile, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - ), - ), + label: Text( + context.lang.shareYourProfile, + style: const TextStyle(fontSize: 13), ), ), ), @@ -353,15 +341,9 @@ class _SearchUsernameView extends State { FontAwesomeIcons.qrcode, size: 14, ), - label: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - context.lang.openYourOwnQRcode, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.bold, - ), - ), + label: Text( + context.lang.openYourOwnQRcode, + style: const TextStyle(fontSize: 13), ), ), ), @@ -371,11 +353,18 @@ class _SearchUsernameView extends State { ), ), const SizedBox(height: 15), - OpenRequestsListComp( - contacts: _openRequestsContacts, - relations: _allAnnouncedUsers, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + OpenRequestsListComp( + contacts: _openRequestsContacts, + relations: _allAnnouncedUsers, + ), + FriendSuggestionsComp(_newAnnouncedUsers), + ], + ), ), - FriendSuggestionsComp(_newAnnouncedUsers), ], ), ), diff --git a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart index 53a3a3c0..6f665b63 100644 --- a/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart +++ b/lib/src/visual/views/contact/add_new_contact_components/open_requests_list.comp.dart @@ -7,6 +7,7 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/headline.element.dart'; @@ -63,8 +64,17 @@ class OpenRequestsListComp extends StatelessWidget { ], ), onPressed: () async { - const update = ContactsCompanion(blocked: Value(true)); - await twonlyDB.contactsDao.updateContact(contact.userId, update); + final block = await showAlertDialog( + context, + context.lang.contactBlockTitle(getContactDisplayName(contact)), + context.lang.contactBlockBody, + ); + if (block) { + const update = ContactsCompanion(blocked: Value(true)); + if (context.mounted) { + await twonlyDB.contactsDao.updateContact(contact.userId, update); + } + } }, ), ), @@ -179,9 +189,7 @@ class OpenRequestsListComp extends StatelessWidget { ), trailing: Row( mainAxisSize: MainAxisSize.min, - children: contact.requested - ? requestedActions(context, contact) - : sendRequestActions(context, contact), + children: contact.requested ? requestedActions(context, contact) : sendRequestActions(context, contact), ), ); }), diff --git a/lib/src/visual/views/home.view.dart b/lib/src/visual/views/home.view.dart index aa955f89..7125e9ba 100644 --- a/lib/src/visual/views/home.view.dart +++ b/lib/src/visual/views/home.view.dart @@ -78,9 +78,7 @@ class HomeViewState extends State { _selectNotificationSub = selectNotificationStream.stream.listen(( response, ) async { - if (response.payload != null && - response.payload!.startsWith(Routes.chats) && - response.payload! != Routes.chats) { + if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) { await routerProvider.push(response.payload!); } streamHomeViewPageIndex.add(0); @@ -116,40 +114,31 @@ class HomeViewState extends State { ); WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.initialPage == 1 && - !userService.currentUser.startWithCameraOpen || - widget.initialPage == 0) { + if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) { streamHomeViewPageIndex.add(0); } }); } Future _initAsync() async { - final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin - .getNotificationAppLaunchDetails(); + final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); RemoteMessage? initialRemoteMessage; try { - initialRemoteMessage = await FirebaseMessaging.instance - .getInitialMessage(); + initialRemoteMessage = await FirebaseMessaging.instance.getInitialMessage(); } catch (e) { Log.error('Could not get initial Firebase message: $e'); } if (widget.initialPage == 0 || initialRemoteMessage != null || - (notificationAppLaunchDetails != null && - notificationAppLaunchDetails.didNotificationLaunchApp)) { + (notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) { if (initialRemoteMessage != null) { Log.info('App launched from iOS/Remote push notification tap.'); streamHomeViewPageIndex.add(0); - } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? - false) { - final payload = - notificationAppLaunchDetails?.notificationResponse?.payload; - if (payload != null && - payload.startsWith(Routes.chats) && - payload != Routes.chats) { + } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { + final payload = notificationAppLaunchDetails?.notificationResponse?.payload; + if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) { await routerProvider.push(payload); streamHomeViewPageIndex.add(0); } @@ -190,25 +179,30 @@ class HomeViewState extends State { _disableCameraTimer?.cancel(); if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) { - if (_activePageIdx == 2 && - notification.metrics.pixels < 100 && - !_isBottomNavVisible) { - setState(() { - _isBottomNavVisible = true; - }); - } else if (notification is ScrollUpdateNotification) { - final delta = notification.scrollDelta ?? 0; - if (delta > 5 && - _isBottomNavVisible && - (_activePageIdx != 2 || notification.metrics.pixels >= 100)) { - setState(() { - _isBottomNavVisible = false; - }); - } else if (delta < -5 && !_isBottomNavVisible) { + final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent; + if (!canScroll) { + if (!_isBottomNavVisible) { setState(() { _isBottomNavVisible = true; }); } + } else { + if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) { + setState(() { + _isBottomNavVisible = true; + }); + } else if (notification is ScrollUpdateNotification) { + final delta = notification.scrollDelta ?? 0; + if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) { + setState(() { + _isBottomNavVisible = false; + }); + } else if (delta < -5 && !_isBottomNavVisible) { + setState(() { + _isBottomNavVisible = true; + }); + } + } } } @@ -244,9 +238,7 @@ class HomeViewState extends State { Widget build(BuildContext context) { return Scaffold( body: GestureDetector( - onDoubleTap: _offsetRatio == 0 - ? _mainCameraController.onDoubleTap - : null, + onDoubleTap: _offsetRatio == 0 ? _mainCameraController.onDoubleTap : null, onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null, child: Stack( children: [ @@ -281,16 +273,12 @@ class HomeViewState extends State { left: 0, top: 0, right: 0, - bottom: (_offsetRatio > 0.25) - ? MediaQuery.sizeOf(context).height * 2 - : 0, + bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0, child: Opacity( opacity: 1 - (_offsetRatio * 4) % 1, child: CameraPreviewControllerView( mainController: _mainCameraController, - isVisible: - ((1 - (_offsetRatio * 4) % 1) == 1) && - _activePageIdx == 1, + isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1, ), ), ), diff --git a/lib/src/visual/views/onboarding/register.view.dart b/lib/src/visual/views/onboarding/register.view.dart index 23668693..8458cd97 100644 --- a/lib/src/visual/views/onboarding/register.view.dart +++ b/lib/src/visual/views/onboarding/register.view.dart @@ -153,21 +153,19 @@ class _RegisterViewState extends State { final isDark = isDarkMode(context); final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white; final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100]; - final sloganColor = isDark - ? Colors.white.withValues(alpha: 0.9) - : Colors.grey[800]; + final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800]; final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600]; return OnboardingWrapper( children: [ - const SizedBox(height: 40), + const SizedBox(height: 30), Center( child: Container( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(10), child: const LinkLogoAnimation(), ), ), - const SizedBox(height: 16), + const SizedBox(height: 12), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text( @@ -180,7 +178,7 @@ class _RegisterViewState extends State { ), ), ), - const SizedBox(height: 48), + const SizedBox(height: 30), Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( @@ -188,9 +186,7 @@ class _RegisterViewState extends State { borderRadius: BorderRadius.circular(32), boxShadow: [ BoxShadow( - color: isDark - ? Colors.black.withValues(alpha: 0.3) - : Colors.black.withValues(alpha: 0.1), + color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10), ), @@ -262,8 +258,7 @@ class _RegisterViewState extends State { ), ), ), - if (_showUserNameError && - usernameController.text.length < 3) ...[ + if (_showUserNameError && usernameController.text.length < 3) ...[ const SizedBox(height: 8), Text( context.lang.registerUsernameLimits, diff --git a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart index 155fbf54..b13ffce5 100644 --- a/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart +++ b/lib/src/visual/views/onboarding/setup/components/mock_contact_request_actions.comp.dart @@ -10,58 +10,64 @@ class MockContactRequestActionsComp extends StatelessWidget { Widget build(BuildContext context) { return IgnorePointer( child: SizedBox( - // width: 125, child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.person_off_rounded, + color: Color.fromARGB(164, 244, 67, 54), + size: 12, + ), + Text( + context.lang.contactActionBlock, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + const SizedBox(height: 4), + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 2, left: 4), + backgroundColor: context.color.surfaceContainerHigh, + foregroundColor: context.color.onSurface, + ), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.check, color: Colors.green, size: 12), + Text( + context.lang.contactActionAccept, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + ], + ), const SizedBox(width: 4), - SizedBox( - height: 20, - // width: 45, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 2, left: 4), - backgroundColor: context.color.surfaceContainerHigh, - foregroundColor: context.color.onSurface, - ), - onPressed: () {}, - child: Row( - children: [ - const Icon( - Icons.person_off_rounded, - color: Color.fromARGB(164, 244, 67, 54), - size: 12, - ), - Text( - context.lang.contactActionBlock, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - ), - const SizedBox(width: 6), - SizedBox( - height: 20, - // width: 50, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 2, left: 4), - backgroundColor: context.color.surfaceContainerHigh, - foregroundColor: context.color.onSurface, - ), - onPressed: () {}, - child: Row( - children: [ - const Icon(Icons.check, color: Colors.green, size: 12), - Text( - context.lang.contactActionAccept, - style: const TextStyle(fontSize: 8), - ), - ], - ), - ), - ), IconButton( style: IconButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 2), @@ -86,28 +92,59 @@ class MockContactSuggestedActionsComp extends StatelessWidget { return IgnorePointer( child: Row( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, children: [ const SizedBox(width: 4), - SizedBox( - height: 20, - child: FilledButton( - style: FilledButton.styleFrom( - padding: const EdgeInsets.only(right: 8, left: 4), - ).merge(secondaryGreyButtonStyle(context)), - onPressed: () {}, - child: Row( - children: [ - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6), - child: FaIcon(FontAwesomeIcons.userPlus, size: 10), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 8, left: 4), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: FaIcon(FontAwesomeIcons.circleQuestion, size: 10), + ), + Text( + context.lang.friendSuggestionsAskFriend, + style: const TextStyle(fontSize: 8), + ), + ], ), - Text( - context.lang.friendSuggestionsRequest, - style: const TextStyle(fontSize: 8), - ), - ], + ), ), - ), + const SizedBox(height: 4), + SizedBox( + height: 20, + child: FilledButton( + style: FilledButton.styleFrom( + padding: const EdgeInsets.only(right: 8, left: 4), + ).merge(secondaryGreyButtonStyle(context)), + onPressed: () {}, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: FaIcon(FontAwesomeIcons.userPlus, size: 10), + ), + Text( + context.lang.friendSuggestionsRequest, + style: const TextStyle(fontSize: 8), + ), + ], + ), + ), + ), + ], ), IconButton( style: IconButton.styleFrom( diff --git a/lib/src/visual/views/public_profile.view.dart b/lib/src/visual/views/public_profile.view.dart index 5783e81f..5d88abd2 100644 --- a/lib/src/visual/views/public_profile.view.dart +++ b/lib/src/visual/views/public_profile.view.dart @@ -10,10 +10,9 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/utils/misc.dart'; -import 'package:twonly/src/visual/components/notification_badge.comp.dart'; +import 'package:twonly/src/visual/components/contact_request_badge.comp.dart'; import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; -import 'package:twonly/src/visual/themes/light.dart'; class PublicProfileView extends StatefulWidget { const PublicProfileView({super.key}); @@ -24,8 +23,6 @@ class PublicProfileView extends StatefulWidget { class _PublicProfileViewState extends State { Uint8List? _publicKey; - int _countContactRequest = 0; - late StreamSubscription _countContactRequestStream; @override void initState() { @@ -36,70 +33,15 @@ class _PublicProfileViewState extends State { Future initAsync() async { _publicKey = await getUserPublicKey(); if (mounted) setState(() {}); - - _countContactRequestStream = twonlyDB.contactsDao - .watchContactsRequestedCount() - .listen((update) { - if (update != null) { - if (!mounted) return; - setState(() { - _countContactRequest = update; - }); - } - }); - } - - @override - void dispose() { - _countContactRequestStream.cancel(); - super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - actions: [ - Stack( - children: (_countContactRequest == 0) - ? [] - : [ - Positioned.fill( - child: Center( - child: Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: primaryColor, - shape: BoxShape.circle, - ), - ), - ), - ), - Center( - child: NotificationBadgeComp( - backgroundColor: isDarkMode(context) - ? Colors.white - : Colors.black, - textColor: isDarkMode(context) - ? Colors.black - : Colors.white, - count: (_countContactRequest).toString(), - child: IconButton( - color: (_countContactRequest > 0) - ? Colors.black - : null, - icon: const FaIcon( - FontAwesomeIcons.userPlus, - size: 18, - ), - onPressed: () => context.push(Routes.chatsAddNewUser), - ), - ), - ), - ], - ), - const SizedBox(width: 15), + actions: const [ + ContactRequestBadgeComp(), + SizedBox(width: 15), ], ), body: Column( @@ -155,8 +97,7 @@ class _PublicProfileViewState extends State { ), onTap: () { final params = ShareParams( - text: - 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}', + text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}', ); SharePlus.instance.share(params); }, diff --git a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart index 4516a8ea..9fb54a9d 100644 --- a/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart +++ b/lib/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart @@ -104,8 +104,7 @@ class UserDiscoverySetupComp extends StatelessWidget { @override Widget build(BuildContext context) { final showShareYourFriends = - showOnlySpecificPage == UserDiscoveryPages.all || - showOnlySpecificPage == UserDiscoveryPages.shareYourFriends; + showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.shareYourFriends; final showLetYourFriendsFindYou = showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou; @@ -172,7 +171,6 @@ class UserDiscoverySetupComp extends StatelessWidget { const SizedBox(height: 8), Center( child: Container( - constraints: const BoxConstraints(maxWidth: 320), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, @@ -334,9 +332,7 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ), subtitle: Text( - context - .lang - .userDiscoverySettingsManualApprovalDesc, + context.lang.userDiscoverySettingsManualApprovalDesc, style: TextStyle( fontSize: 11, color: context.color.onSurfaceVariant, @@ -350,16 +346,13 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ], ), - crossFadeState: state.isUserDiscoveryEnabled - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: state.isUserDiscoveryEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 300), ), ], ), ), - if (showOnlySpecificPage == UserDiscoveryPages.all) - const SizedBox(height: 48), + if (showOnlySpecificPage == UserDiscoveryPages.all) const SizedBox(height: 48), ], if (showLetYourFriendsFindYou) ...[ Text( @@ -418,7 +411,6 @@ class UserDiscoverySetupComp extends StatelessWidget { const SizedBox(height: 8), Center( child: Container( - constraints: const BoxConstraints(maxWidth: 320), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, @@ -483,7 +475,6 @@ class UserDiscoverySetupComp extends StatelessWidget { const SizedBox(height: 8), Center( child: Container( - constraints: const BoxConstraints(maxWidth: 320), padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, @@ -592,9 +583,7 @@ class UserDiscoverySetupComp extends StatelessWidget { children: [ Expanded( child: Text( - context - .lang - .userDiscoverySettingsMutualFriends, + context.lang.userDiscoverySettingsMutualFriends, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -610,10 +599,9 @@ class UserDiscoverySetupComp extends StatelessWidget { color: context.color.surface, borderRadius: BorderRadius.circular(8), border: Border.all( - color: context.color.outlineVariant - .withValues( - alpha: 0.5, - ), + color: context.color.outlineVariant.withValues( + alpha: 0.5, + ), ), ), child: DropdownButtonHideUnderline( @@ -648,9 +636,7 @@ class UserDiscoverySetupComp extends StatelessWidget { ), ], ), - crossFadeState: state.sharePromotion - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + crossFadeState: state.sharePromotion ? CrossFadeState.showSecond : CrossFadeState.showFirst, duration: const Duration(milliseconds: 300), ), ],