Improved: Onboarding flow for new users.

This commit is contained in:
otsmr 2026-05-20 00:47:45 +02:00
parent c0e45cfe1f
commit 2d6a2e436f
15 changed files with 660 additions and 550 deletions

View file

@ -3,6 +3,7 @@
## 0.2.17 ## 0.2.17
- New: Adds an "Ask a Friend" button to new contact suggestions. - 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. - Improved: The blue verification checkmark now displays the total number of verifications.
- Fix: Issue with receiving messages when user closed app while decrypting - Fix: Issue with receiving messages when user closed app while decrypting
- Fix: Background message fetching reliability. - Fix: Background message fetching reliability.

View file

@ -137,12 +137,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _isLoaded = false; bool _isLoaded = false;
bool _isTwonlyLocked = true; bool _isTwonlyLocked = true;
bool _wasLogged = true; bool _wasLogged = true;
late int _initialPage;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initialPage = widget.initialPage;
Log.info('AppWidgetState: initState started'); Log.info('AppWidgetState: initState started');
initAsync(); initAsync();
} }
@ -150,6 +152,12 @@ class _AppMainWidgetState extends State<AppMainWidget> {
Future<void> initAsync() async { Future<void> initAsync() async {
Log.info('AppWidgetState: initAsync started'); Log.info('AppWidgetState: initAsync started');
if (userService.isUserCreated) { if (userService.isUserCreated) {
if (_initialPage != 0) {
final count = await twonlyDB.contactsDao.getContactsCount();
if (count == 0) {
_initialPage = 0;
}
}
try { try {
unawaited(FirebaseMessaging.instance.requestPermission()); unawaited(FirebaseMessaging.instance.requestPermission());
} catch (e) { } catch (e) {
@ -200,8 +208,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isTwonlyLocked = false; _isTwonlyLocked = false;
}), }),
); );
} else if (!userService.currentUser.skipSetupPages && } else if (!userService.currentUser.skipSetupPages && userService.currentUser.currentSetupPage != null) {
userService.currentUser.currentSetupPage != null) {
// This will only be shown in case the user have not skipped // This will only be shown in case the user have not skipped
child = SetupView( child = SetupView(
onUpdate: () => setState(() { onUpdate: () => setState(() {
@ -210,7 +217,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
); );
} else { } else {
child = HomeView( child = HomeView(
initialPage: widget.initialPage, initialPage: _initialPage,
); );
} }
} else if (_showOnboarding) { } else if (_showOnboarding) {

View file

@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
return select(contacts).get(); return select(contacts).get();
} }
Future<int> 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<int?> watchContactsBlocked() { Stream<int?> watchContactsBlocked() {
final count = contacts.userId.count(); final count = contacts.userId.count();
final query = selectOnly(contacts) final query = selectOnly(contacts)

View file

@ -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<int?>(
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),
),
),
),
],
);
},
);
}
}

View file

@ -44,17 +44,15 @@ class _ProfileQrCodeCompState extends State<ProfileQrCodeComp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading || _qrCode == null) { final loaded = !_isLoading && _qrCode != null;
return SizedBox( return SizedBox(
width: widget.size, width: widget.size,
height: widget.size, height: widget.size,
child: const Center( child: AnimatedSwitcher(
child: CircularProgressIndicator(), duration: const Duration(milliseconds: 150),
), child: loaded
); ? Container(
} key: const ValueKey('qr_code_container'),
return Container(
// padding: const EdgeInsets.all(3), // padding: const EdgeInsets.all(3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.color.primary, color: context.color.primary,
@ -81,9 +79,7 @@ class _ProfileQrCodeCompState extends State<ProfileQrCodeComp> {
borderRadius: 2, borderRadius: 2,
), ),
gapless: false, gapless: false,
embeddedImage: (widget.showAvatar && _userAvatar != null) embeddedImage: (widget.showAvatar && _userAvatar != null) ? MemoryImage(_userAvatar!) : null,
? MemoryImage(_userAvatar!)
: null,
embeddedImageStyle: QrEmbeddedImageStyle( embeddedImageStyle: QrEmbeddedImageStyle(
size: const Size(60, 66), size: const Size(60, 66),
embeddedImageShape: EmbeddedImageShape.square, embeddedImageShape: EmbeddedImageShape.square,
@ -92,6 +88,9 @@ class _ProfileQrCodeCompState extends State<ProfileQrCodeComp> {
), ),
size: widget.size, size: widget.size,
), ),
)
: const SizedBox.shrink(key: ValueKey('qr_code_placeholder')),
),
); );
} }
} }

View file

@ -3,9 +3,7 @@ import 'dart:collection';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.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/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.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/components/flame_counter.comp.dart';
import 'package:twonly/src/visual/decorations/input_text.decoration.dart'; import 'package:twonly/src/visual/decorations/input_text.decoration.dart';
import 'package:twonly/src/visual/elements/headline.element.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/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_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/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 { class ShareImageView extends StatefulWidget {
const ShareImageView({ const ShareImageView({
@ -111,9 +111,7 @@ class _ShareImageView extends State<ShareImageView> {
for (final group in groups) { for (final group in groups) {
if (group.pinned) continue; if (group.pinned) continue;
if (!group.archived && if (!group.archived && getFlameCounterFromGroup(group).counter > 0 && bestFriends.length < 6) {
getFlameCounterFromGroup(group).counter > 0 &&
bestFriends.length < 6) {
bestFriends.add(group); bestFriends.add(group);
} else { } else {
otherUsers.add(group); otherUsers.add(group);
@ -133,10 +131,7 @@ class _ShareImageView extends State<ShareImageView> {
await updateGroups( await updateGroups(
_allGroups _allGroups
.where( .where(
(x) => (x) => !x.archived || !hideArchivedUsers || widget.selectedGroupIds.contains(x.groupId),
!x.archived ||
!hideArchivedUsers ||
widget.selectedGroupIds.contains(x.groupId),
) )
.toList(), .toList(),
); );
@ -160,31 +155,23 @@ class _ShareImageView extends State<ShareImageView> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.shareImageTitle), title: Text(context.lang.shareImageTitle),
actions: const [
ContactRequestBadgeComp(),
SizedBox(width: 15),
],
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
bottom: 40, bottom: 40,
left: 10, left: 10,
top: 20,
right: 10, right: 10,
), ),
child: Column( child: ListView(
children: [ children: [
if (_allGroups.isEmpty) if (_allGroups.isEmpty)
Expanded( const EmptyChatListComp()
child: Center( else ...[
child: FilledButton.icon(
icon: const Icon(Icons.person_add),
onPressed: () => context.push(Routes.chatsAddNewUser),
label: Text(
context.lang.chatListViewSearchUserNameBtn,
),
),
),
),
if (_allGroups.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding: const EdgeInsets.symmetric(horizontal: 10),
child: TextField( child: TextField(
@ -206,8 +193,7 @@ class _ShareImageView extends State<ShareImageView> {
selectedGroupIds: widget.selectedGroupIds, selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds, updateSelectedGroupIds: updateSelectedGroupIds,
title: context.lang.shareImagePinnedContacts, title: context.lang.shareImagePinnedContacts,
showSelectAll: showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
!widget.mediaFileService.mediaFile.requiresAuthentication,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
BestFriendsSelector( BestFriendsSelector(
@ -215,8 +201,7 @@ class _ShareImageView extends State<ShareImageView> {
selectedGroupIds: widget.selectedGroupIds, selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds, updateSelectedGroupIds: updateSelectedGroupIds,
title: context.lang.shareImageBestFriends, title: context.lang.shareImageBestFriends,
showSelectAll: showSelectAll: !widget.mediaFileService.mediaFile.requiresAuthentication,
!widget.mediaFileService.mediaFile.requiresAuthentication,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (_otherUsers.isNotEmpty) if (_otherUsers.isNotEmpty)
@ -259,18 +244,19 @@ class _ShareImageView extends State<ShareImageView> {
], ],
), ),
if (_otherUsers.isNotEmpty) if (_otherUsers.isNotEmpty)
Expanded( UserList(
child: UserList(
List.from(_otherUsers), List.from(_otherUsers),
selectedGroupIds: widget.selectedGroupIds, selectedGroupIds: widget.selectedGroupIds,
updateSelectedGroupIds: updateSelectedGroupIds, updateSelectedGroupIds: updateSelectedGroupIds,
), ),
), ],
], ],
), ),
), ),
), ),
floatingActionButton: SizedBox( floatingActionButton: _allGroups.isEmpty
? null
: SizedBox(
height: 168, height: 168,
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 20, right: 20), padding: const EdgeInsets.only(bottom: 20, right: 20),
@ -313,8 +299,7 @@ class _ShareImageView extends State<ShareImageView> {
) )
: const FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (!mediaStoreFutureReady || if (!mediaStoreFutureReady || widget.selectedGroupIds.isEmpty) {
widget.selectedGroupIds.isEmpty) {
return; return;
} }
@ -375,6 +360,8 @@ class UserList extends StatelessWidget {
); );
return ListView.builder( return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
restorationId: 'new_message_users_list', restorationId: 'new_message_users_list',
itemCount: groups.length, itemCount: groups.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {

View file

@ -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/connection_status.comp.dart';
import 'package:twonly/src/visual/components/notification_badge.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/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/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/chats/chat_list_components/group_list_item.comp.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/finish_setup.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<ChatListView> { class _ChatListViewState extends State<ChatListView> {
StreamSubscription<void>? _userSub; StreamSubscription<void>? _userSub;
late StreamSubscription<List<Group>> _contactsSub; StreamSubscription<List<Group>>? _contactsSub;
StreamSubscription<List<Contact>>? _contactsCountSub;
List<Group> _groupsNotPinned = []; List<Group> _groupsNotPinned = [];
List<Group> _groupsPinned = []; List<Group> _groupsPinned = [];
List<Group> _groupsArchived = []; List<Group> _groupsArchived = [];
bool _hasContacts = true;
bool get _hasOpenGroup => _groupsNotPinned.isNotEmpty || _groupsArchived.isNotEmpty || _groupsPinned.isNotEmpty;
GlobalKey searchForOtherUsers = GlobalKey(); GlobalKey searchForOtherUsers = GlobalKey();
bool showFeedbackShortcut = false; bool showFeedbackShortcut = false;
@ -58,17 +63,20 @@ class _ChatListViewState extends State<ChatListView> {
_contactsSub = stream.listen((groups) { _contactsSub = stream.listen((groups) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_groupsNotPinned = groups _groupsNotPinned = groups.where((x) => !x.pinned && !x.archived).toList();
.where((x) => !x.pinned && !x.archived)
.toList();
_groupsPinned = groups.where((x) => x.pinned && !x.archived).toList(); _groupsPinned = groups.where((x) => x.pinned && !x.archived).toList();
_groupsArchived = groups.where((x) => x.archived).toList(); _groupsArchived = groups.where((x) => x.archived).toList();
}); });
}); });
_countContactRequestStream = twonlyDB.contactsDao _contactsCountSub = twonlyDB.contactsDao.watchAllAcceptedContacts().listen((contacts) {
.watchContactsRequestedCount() if (!mounted) return;
.listen((update) { setState(() {
_hasContacts = contacts.isNotEmpty;
});
});
_countContactRequestStream = twonlyDB.contactsDao.watchContactsRequestedCount().listen((update) {
if (update != null) { if (update != null) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@ -77,9 +85,7 @@ class _ChatListViewState extends State<ChatListView> {
} }
}); });
_countAnnouncedStream = twonlyDB.userDiscoveryDao _countAnnouncedStream = twonlyDB.userDiscoveryDao.watchNewAnnouncementsWithDataCount().listen((update) {
.watchNewAnnouncementsWithDataCount()
.listen((update) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_countAnnouncedUsers = update; _countAnnouncedUsers = update;
@ -93,8 +99,7 @@ class _ChatListViewState extends State<ChatListView> {
changeLog.codeUnits, changeLog.codeUnits,
)).bytes; )).bytes;
if (!userService.currentUser.hideChangeLog && if (!userService.currentUser.hideChangeLog &&
userService.currentUser.lastChangeLogHash.toString() != userService.currentUser.lastChangeLogHash.toString() != changeLogHash.toString()) {
changeLogHash.toString()) {
await UserService.update((u) { await UserService.update((u) {
u.lastChangeLogHash = changeLogHash; u.lastChangeLogHash = changeLogHash;
}); });
@ -113,7 +118,8 @@ class _ChatListViewState extends State<ChatListView> {
@override @override
void dispose() { void dispose() {
_contactsSub.cancel(); _contactsSub?.cancel();
_contactsCountSub?.cancel();
_countContactRequestStream.cancel(); _countContactRequestStream.cancel();
_countAnnouncedStream.cancel(); _countAnnouncedStream.cancel();
_userSub?.cancel(); _userSub?.cancel();
@ -182,16 +188,11 @@ class _ChatListViewState extends State<ChatListView> {
), ),
Center( Center(
child: NotificationBadgeComp( child: NotificationBadgeComp(
backgroundColor: isDarkMode(context) backgroundColor: isDarkMode(context) ? Colors.white : Colors.black,
? Colors.white
: Colors.black,
textColor: isDarkMode(context) ? Colors.black : Colors.white, textColor: isDarkMode(context) ? Colors.black : Colors.white,
count: (_countAnnouncedUsers + _countContactRequest) count: (_countAnnouncedUsers + _countContactRequest).toString(),
.toString(),
child: IconButton( child: IconButton(
color: (_countAnnouncedUsers + _countContactRequest > 0) color: (_countAnnouncedUsers + _countContactRequest > 0) ? Colors.black : null,
? Colors.black
: null,
key: searchForOtherUsers, key: searchForOtherUsers,
icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18), icon: const FaIcon(FontAwesomeIcons.userPlus, size: 18),
onPressed: () => context.push(Routes.chatsAddNewUser), onPressed: () => context.push(Routes.chatsAddNewUser),
@ -217,21 +218,11 @@ class _ChatListViewState extends State<ChatListView> {
children: [ children: [
const FinishSetupComp(), const FinishSetupComp(),
const MissingBackupComp(), const MissingBackupComp(),
if (_groupsNotPinned.isEmpty && if (!_hasOpenGroup)
_groupsPinned.isEmpty &&
_groupsArchived.isEmpty)
Expanded( Expanded(
child: Center( child: ListView(
child: Padding( physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(10), children: const [EmptyChatListComp()],
child: FilledButton.icon(
icon: const Icon(Icons.person_add),
onPressed: () => context.push(Routes.chatsAddNewUser),
label: Text(
context.lang.chatListViewSearchUserNameBtn,
),
),
),
), ),
) )
else else
@ -243,10 +234,7 @@ class _ChatListViewState extends State<ChatListView> {
_groupsNotPinned.length + _groupsNotPinned.length +
(_groupsArchived.isNotEmpty ? 1 : 0), (_groupsArchived.isNotEmpty ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= if (index >= _groupsNotPinned.length + _groupsPinned.length + (_groupsPinned.isNotEmpty ? 1 : 0)) {
_groupsNotPinned.length +
_groupsPinned.length +
(_groupsPinned.isNotEmpty ? 1 : 0)) {
if (_groupsArchived.isEmpty) return Container(); if (_groupsArchived.isEmpty) return Container();
return ListTile( return ListTile(
title: Text( title: Text(
@ -289,7 +277,9 @@ class _ChatListViewState extends State<ChatListView> {
], ],
), ),
), ),
floatingActionButton: Padding( floatingActionButton: !_hasContacts
? null
: Padding(
padding: const EdgeInsets.only(bottom: 30), padding: const EdgeInsets.only(bottom: 30),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,

View file

@ -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<void> _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),
],
),
);
}
}

View file

@ -62,18 +62,15 @@ class _SearchUsernameView extends State<AddNewUserView> {
} }
}, },
); );
_newAnnouncedUsersStream = twonlyDB.userDiscoveryDao
.watchNewAnnouncedUsersWithRelations() _newAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchNewAnnouncedUsersWithRelations().listen((update) {
.listen((update) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_newAnnouncedUsers = update; _newAnnouncedUsers = update;
}); });
} }
}); });
_allAnnouncedUsersStream = twonlyDB.userDiscoveryDao _allAnnouncedUsersStream = twonlyDB.userDiscoveryDao.watchAllAnnouncedUsersWithRelations().listen((update) {
.watchAllAnnouncedUsersWithRelations()
.listen((update) {
if (mounted) { if (mounted) {
setState(() { setState(() {
_allAnnouncedUsers = update; _allAnnouncedUsers = update;
@ -93,8 +90,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
Future<void> _shareProfile() async { Future<void> _shareProfile() async {
final pubKey = await getUserPublicKey(); final pubKey = await getUserPublicKey();
final params = ShareParams( final params = ShareParams(
text: text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(pubKey)}',
); );
await SharePlus.instance.share(params); await SharePlus.instance.share(params);
} }
@ -194,9 +190,7 @@ class _SearchUsernameView extends State<AddNewUserView> {
), ),
); );
if (widget.publicKey != null && if (widget.publicKey != null && mounted && widget.publicKey!.equals(userdata.publicIdentityKey)) {
mounted &&
widget.publicKey!.equals(userdata.publicIdentityKey)) {
final markAsVerified = await showAlertDialog( final markAsVerified = await showAlertDialog(
context, context,
context.lang.linkFromUsername(username), context.lang.linkFromUsername(username),
@ -321,15 +315,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
FontAwesomeIcons.shareNodes, FontAwesomeIcons.shareNodes,
size: 14, size: 14,
), ),
label: FittedBox( label: Text(
fit: BoxFit.scaleDown,
child: Text(
context.lang.shareYourProfile, context.lang.shareYourProfile,
style: const TextStyle( style: const TextStyle(fontSize: 13),
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
), ),
@ -353,15 +341,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
FontAwesomeIcons.qrcode, FontAwesomeIcons.qrcode,
size: 14, size: 14,
), ),
label: FittedBox( label: Text(
fit: BoxFit.scaleDown,
child: Text(
context.lang.openYourOwnQRcode, context.lang.openYourOwnQRcode,
style: const TextStyle( style: const TextStyle(fontSize: 13),
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
), ),
@ -371,6 +353,10 @@ class _SearchUsernameView extends State<AddNewUserView> {
), ),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
OpenRequestsListComp( OpenRequestsListComp(
contacts: _openRequestsContacts, contacts: _openRequestsContacts,
relations: _allAnnouncedUsers, relations: _allAnnouncedUsers,
@ -379,6 +365,9 @@ class _SearchUsernameView extends State<AddNewUserView> {
], ],
), ),
), ),
],
),
),
); );
} }
} }

View file

@ -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/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.api.dart'; import 'package:twonly/src/services/api/messages.api.dart';
import 'package:twonly/src/utils/misc.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/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart';
import 'package:twonly/src/visual/elements/headline.element.dart'; import 'package:twonly/src/visual/elements/headline.element.dart';
@ -63,8 +64,17 @@ class OpenRequestsListComp extends StatelessWidget {
], ],
), ),
onPressed: () async { onPressed: () async {
final block = await showAlertDialog(
context,
context.lang.contactBlockTitle(getContactDisplayName(contact)),
context.lang.contactBlockBody,
);
if (block) {
const update = ContactsCompanion(blocked: Value(true)); const update = ContactsCompanion(blocked: Value(true));
if (context.mounted) {
await twonlyDB.contactsDao.updateContact(contact.userId, update); await twonlyDB.contactsDao.updateContact(contact.userId, update);
}
}
}, },
), ),
), ),
@ -179,9 +189,7 @@ class OpenRequestsListComp extends StatelessWidget {
), ),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: contact.requested children: contact.requested ? requestedActions(context, contact) : sendRequestActions(context, contact),
? requestedActions(context, contact)
: sendRequestActions(context, contact),
), ),
); );
}), }),

View file

@ -78,9 +78,7 @@ class HomeViewState extends State<HomeView> {
_selectNotificationSub = selectNotificationStream.stream.listen(( _selectNotificationSub = selectNotificationStream.stream.listen((
response, response,
) async { ) async {
if (response.payload != null && if (response.payload != null && response.payload!.startsWith(Routes.chats) && response.payload! != Routes.chats) {
response.payload!.startsWith(Routes.chats) &&
response.payload! != Routes.chats) {
await routerProvider.push(response.payload!); await routerProvider.push(response.payload!);
} }
streamHomeViewPageIndex.add(0); streamHomeViewPageIndex.add(0);
@ -116,40 +114,31 @@ class HomeViewState extends State<HomeView> {
); );
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.initialPage == 1 && if (widget.initialPage == 1 && !userService.currentUser.startWithCameraOpen || widget.initialPage == 0) {
!userService.currentUser.startWithCameraOpen ||
widget.initialPage == 0) {
streamHomeViewPageIndex.add(0); streamHomeViewPageIndex.add(0);
} }
}); });
} }
Future<void> _initAsync() async { Future<void> _initAsync() async {
final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
.getNotificationAppLaunchDetails();
RemoteMessage? initialRemoteMessage; RemoteMessage? initialRemoteMessage;
try { try {
initialRemoteMessage = await FirebaseMessaging.instance initialRemoteMessage = await FirebaseMessaging.instance.getInitialMessage();
.getInitialMessage();
} catch (e) { } catch (e) {
Log.error('Could not get initial Firebase message: $e'); Log.error('Could not get initial Firebase message: $e');
} }
if (widget.initialPage == 0 || if (widget.initialPage == 0 ||
initialRemoteMessage != null || initialRemoteMessage != null ||
(notificationAppLaunchDetails != null && (notificationAppLaunchDetails != null && notificationAppLaunchDetails.didNotificationLaunchApp)) {
notificationAppLaunchDetails.didNotificationLaunchApp)) {
if (initialRemoteMessage != null) { if (initialRemoteMessage != null) {
Log.info('App launched from iOS/Remote push notification tap.'); Log.info('App launched from iOS/Remote push notification tap.');
streamHomeViewPageIndex.add(0); streamHomeViewPageIndex.add(0);
} else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? } else if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
false) { final payload = notificationAppLaunchDetails?.notificationResponse?.payload;
final payload = if (payload != null && payload.startsWith(Routes.chats) && payload != Routes.chats) {
notificationAppLaunchDetails?.notificationResponse?.payload;
if (payload != null &&
payload.startsWith(Routes.chats) &&
payload != Routes.chats) {
await routerProvider.push(payload); await routerProvider.push(payload);
streamHomeViewPageIndex.add(0); streamHomeViewPageIndex.add(0);
} }
@ -190,17 +179,21 @@ class HomeViewState extends State<HomeView> {
_disableCameraTimer?.cancel(); _disableCameraTimer?.cancel();
if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) { if (notification.depth > 0 && notification.metrics.axis == Axis.vertical) {
if (_activePageIdx == 2 && final canScroll = notification.metrics.maxScrollExtent > notification.metrics.minScrollExtent;
notification.metrics.pixels < 100 && if (!canScroll) {
!_isBottomNavVisible) { if (!_isBottomNavVisible) {
setState(() {
_isBottomNavVisible = true;
});
}
} else {
if (_activePageIdx == 2 && notification.metrics.pixels < 100 && !_isBottomNavVisible) {
setState(() { setState(() {
_isBottomNavVisible = true; _isBottomNavVisible = true;
}); });
} else if (notification is ScrollUpdateNotification) { } else if (notification is ScrollUpdateNotification) {
final delta = notification.scrollDelta ?? 0; final delta = notification.scrollDelta ?? 0;
if (delta > 5 && if (delta > 5 && _isBottomNavVisible && (_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
_isBottomNavVisible &&
(_activePageIdx != 2 || notification.metrics.pixels >= 100)) {
setState(() { setState(() {
_isBottomNavVisible = false; _isBottomNavVisible = false;
}); });
@ -211,6 +204,7 @@ class HomeViewState extends State<HomeView> {
} }
} }
} }
}
if (notification.depth == 0 && notification is ScrollUpdateNotification) { if (notification.depth == 0 && notification is ScrollUpdateNotification) {
setState(() { setState(() {
@ -244,9 +238,7 @@ class HomeViewState extends State<HomeView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onDoubleTap: _offsetRatio == 0 onDoubleTap: _offsetRatio == 0 ? _mainCameraController.onDoubleTap : null,
? _mainCameraController.onDoubleTap
: null,
onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null, onTapDown: _offsetRatio == 0 ? _mainCameraController.onTapDown : null,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
@ -281,16 +273,12 @@ class HomeViewState extends State<HomeView> {
left: 0, left: 0,
top: 0, top: 0,
right: 0, right: 0,
bottom: (_offsetRatio > 0.25) bottom: (_offsetRatio > 0.25) ? MediaQuery.sizeOf(context).height * 2 : 0,
? MediaQuery.sizeOf(context).height * 2
: 0,
child: Opacity( child: Opacity(
opacity: 1 - (_offsetRatio * 4) % 1, opacity: 1 - (_offsetRatio * 4) % 1,
child: CameraPreviewControllerView( child: CameraPreviewControllerView(
mainController: _mainCameraController, mainController: _mainCameraController,
isVisible: isVisible: ((1 - (_offsetRatio * 4) % 1) == 1) && _activePageIdx == 1,
((1 - (_offsetRatio * 4) % 1) == 1) &&
_activePageIdx == 1,
), ),
), ),
), ),

View file

@ -153,21 +153,19 @@ class _RegisterViewState extends State<RegisterView> {
final isDark = isDarkMode(context); final isDark = isDarkMode(context);
final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white; final cardColor = isDark ? const Color(0xFF1E293B) : Colors.white;
final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100]; final inputColor = isDark ? const Color(0xFF0F172A) : Colors.grey[100];
final sloganColor = isDark final sloganColor = isDark ? Colors.white.withValues(alpha: 0.9) : Colors.grey[800];
? Colors.white.withValues(alpha: 0.9)
: Colors.grey[800];
final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600]; final secondaryButtonColor = isDark ? Colors.grey[400] : Colors.grey[600];
return OnboardingWrapper( return OnboardingWrapper(
children: [ children: [
const SizedBox(height: 40), const SizedBox(height: 30),
Center( Center(
child: Container( child: Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(10),
child: const LinkLogoAnimation(), child: const LinkLogoAnimation(),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 12),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text( child: Text(
@ -180,7 +178,7 @@ class _RegisterViewState extends State<RegisterView> {
), ),
), ),
), ),
const SizedBox(height: 48), const SizedBox(height: 30),
Container( Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -188,9 +186,7 @@ class _RegisterViewState extends State<RegisterView> {
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: isDark color: isDark ? Colors.black.withValues(alpha: 0.3) : Colors.black.withValues(alpha: 0.1),
? Colors.black.withValues(alpha: 0.3)
: Colors.black.withValues(alpha: 0.1),
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, 10), offset: const Offset(0, 10),
), ),
@ -262,8 +258,7 @@ class _RegisterViewState extends State<RegisterView> {
), ),
), ),
), ),
if (_showUserNameError && if (_showUserNameError && usernameController.text.length < 3) ...[
usernameController.text.length < 3) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
context.lang.registerUsernameLimits, context.lang.registerUsernameLimits,

View file

@ -10,14 +10,16 @@ class MockContactRequestActionsComp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IgnorePointer( return IgnorePointer(
child: SizedBox( child: SizedBox(
// width: 125,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
const SizedBox(width: 4),
SizedBox( SizedBox(
height: 20, height: 20,
// width: 45,
child: FilledButton( child: FilledButton(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: const EdgeInsets.only(right: 2, left: 4), padding: const EdgeInsets.only(right: 2, left: 4),
@ -26,6 +28,7 @@ class MockContactRequestActionsComp extends StatelessWidget {
), ),
onPressed: () {}, onPressed: () {},
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon( const Icon(
Icons.person_off_rounded, Icons.person_off_rounded,
@ -40,10 +43,9 @@ class MockContactRequestActionsComp extends StatelessWidget {
), ),
), ),
), ),
const SizedBox(width: 6), const SizedBox(height: 4),
SizedBox( SizedBox(
height: 20, height: 20,
// width: 50,
child: FilledButton( child: FilledButton(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
padding: const EdgeInsets.only(right: 2, left: 4), padding: const EdgeInsets.only(right: 2, left: 4),
@ -52,6 +54,7 @@ class MockContactRequestActionsComp extends StatelessWidget {
), ),
onPressed: () {}, onPressed: () {},
child: Row( child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.check, color: Colors.green, size: 12), const Icon(Icons.check, color: Colors.green, size: 12),
Text( Text(
@ -62,6 +65,9 @@ class MockContactRequestActionsComp extends StatelessWidget {
), ),
), ),
), ),
],
),
const SizedBox(width: 4),
IconButton( IconButton(
style: IconButton.styleFrom( style: IconButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),
@ -86,8 +92,13 @@ class MockContactSuggestedActionsComp extends StatelessWidget {
return IgnorePointer( return IgnorePointer(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
const SizedBox(width: 4), const SizedBox(width: 4),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox( SizedBox(
height: 20, height: 20,
child: FilledButton( child: FilledButton(
@ -96,6 +107,30 @@ class MockContactSuggestedActionsComp extends StatelessWidget {
).merge(secondaryGreyButtonStyle(context)), ).merge(secondaryGreyButtonStyle(context)),
onPressed: () {}, onPressed: () {},
child: Row( 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),
),
],
),
),
),
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: [ children: [
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 6), padding: EdgeInsets.symmetric(horizontal: 6),
@ -109,6 +144,8 @@ class MockContactSuggestedActionsComp extends StatelessWidget {
), ),
), ),
), ),
],
),
IconButton( IconButton(
style: IconButton.styleFrom( style: IconButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 2), padding: const EdgeInsets.symmetric(horizontal: 2),

View file

@ -10,10 +10,9 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/utils/misc.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/components/profile_qr_code.comp.dart';
import 'package:twonly/src/visual/elements/better_list_title.element.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
class PublicProfileView extends StatefulWidget { class PublicProfileView extends StatefulWidget {
const PublicProfileView({super.key}); const PublicProfileView({super.key});
@ -24,8 +23,6 @@ class PublicProfileView extends StatefulWidget {
class _PublicProfileViewState extends State<PublicProfileView> { class _PublicProfileViewState extends State<PublicProfileView> {
Uint8List? _publicKey; Uint8List? _publicKey;
int _countContactRequest = 0;
late StreamSubscription<int?> _countContactRequestStream;
@override @override
void initState() { void initState() {
@ -36,70 +33,15 @@ class _PublicProfileViewState extends State<PublicProfileView> {
Future<void> initAsync() async { Future<void> initAsync() async {
_publicKey = await getUserPublicKey(); _publicKey = await getUserPublicKey();
if (mounted) setState(() {}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
actions: [ actions: const [
Stack( ContactRequestBadgeComp(),
children: (_countContactRequest == 0) SizedBox(width: 15),
? []
: [
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),
], ],
), ),
body: Column( body: Column(
@ -155,8 +97,7 @@ class _PublicProfileViewState extends State<PublicProfileView> {
), ),
onTap: () { onTap: () {
final params = ShareParams( final params = ShareParams(
text: text: 'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}',
'https://me.twonly.eu/${userService.currentUser.username}#${base64Url.encode(_publicKey!)}',
); );
SharePlus.instance.share(params); SharePlus.instance.share(params);
}, },

View file

@ -104,8 +104,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final showShareYourFriends = final showShareYourFriends =
showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.shareYourFriends;
showOnlySpecificPage == UserDiscoveryPages.shareYourFriends;
final showLetYourFriendsFindYou = final showLetYourFriendsFindYou =
showOnlySpecificPage == UserDiscoveryPages.all || showOnlySpecificPage == UserDiscoveryPages.all ||
showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou; showOnlySpecificPage == UserDiscoveryPages.letYourFriendsFindYou;
@ -172,7 +171,6 @@ class UserDiscoverySetupComp extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 320),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
@ -334,9 +332,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
), ),
), ),
subtitle: Text( subtitle: Text(
context context.lang.userDiscoverySettingsManualApprovalDesc,
.lang
.userDiscoverySettingsManualApprovalDesc,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: context.color.onSurfaceVariant, color: context.color.onSurfaceVariant,
@ -350,16 +346,13 @@ class UserDiscoverySetupComp extends StatelessWidget {
), ),
], ],
), ),
crossFadeState: state.isUserDiscoveryEnabled crossFadeState: state.isUserDiscoveryEnabled ? CrossFadeState.showSecond : CrossFadeState.showFirst,
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
), ),
], ],
), ),
), ),
if (showOnlySpecificPage == UserDiscoveryPages.all) if (showOnlySpecificPage == UserDiscoveryPages.all) const SizedBox(height: 48),
const SizedBox(height: 48),
], ],
if (showLetYourFriendsFindYou) ...[ if (showLetYourFriendsFindYou) ...[
Text( Text(
@ -418,7 +411,6 @@ class UserDiscoverySetupComp extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 320),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
@ -483,7 +475,6 @@ class UserDiscoverySetupComp extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Center( Center(
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 320),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
@ -592,9 +583,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
context context.lang.userDiscoverySettingsMutualFriends,
.lang
.userDiscoverySettingsMutualFriends,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -610,8 +599,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
color: context.color.surface, color: context.color.surface,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: context.color.outlineVariant color: context.color.outlineVariant.withValues(
.withValues(
alpha: 0.5, alpha: 0.5,
), ),
), ),
@ -648,9 +636,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
), ),
], ],
), ),
crossFadeState: state.sharePromotion crossFadeState: state.sharePromotion ? CrossFadeState.showSecond : CrossFadeState.showFirst,
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
), ),
], ],