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
- 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.

View file

@ -137,12 +137,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _isLoaded = false;
bool _isTwonlyLocked = true;
bool _wasLogged = true;
late int _initialPage;
(Future<int>?, 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<AppMainWidget> {
Future<void> 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<AppMainWidget> {
_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<AppMainWidget> {
);
} else {
child = HomeView(
initialPage: widget.initialPage,
initialPage: _initialPage,
);
}
} else if (_showOnboarding) {

View file

@ -103,6 +103,13 @@ class ContactsDao extends DatabaseAccessor<TwonlyDB> with _$ContactsDaoMixin {
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() {
final count = contacts.userId.count();
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,53 +44,52 @@ class _ProfileQrCodeCompState extends State<ProfileQrCodeComp> {
@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')),
),
);
}

View file

@ -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<ShareImageView> {
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<ShareImageView> {
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<ShareImageView> {
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<ShareImageView> {
),
),
),
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<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
!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<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
),
backgroundColor: WidgetStateProperty.all<Color>(
!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) {

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/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<ChatListView> {
StreamSubscription<void>? _userSub;
late StreamSubscription<List<Group>> _contactsSub;
StreamSubscription<List<Group>>? _contactsSub;
StreamSubscription<List<Contact>>? _contactsCountSub;
List<Group> _groupsNotPinned = [];
List<Group> _groupsPinned = [];
List<Group> _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<ChatListView> {
_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<ChatListView> {
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<ChatListView> {
@override
void dispose() {
_contactsSub.cancel();
_contactsSub?.cancel();
_contactsCountSub?.cancel();
_countContactRequestStream.cancel();
_countAnnouncedStream.cancel();
_userSub?.cancel();
@ -182,16 +188,11 @@ class _ChatListViewState extends State<ChatListView> {
),
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<ChatListView> {
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<ChatListView> {
_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<ChatListView> {
],
),
),
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,
),
),
],
),
),
);
}
}

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,24 +62,21 @@ class _SearchUsernameView extends State<AddNewUserView> {
}
},
);
_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<AddNewUserView> {
Future<void> _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<AddNewUserView> {
),
);
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<AddNewUserView> {
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<AddNewUserView> {
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<AddNewUserView> {
),
),
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),
],
),
),

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/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),
),
);
}),

View file

@ -78,9 +78,7 @@ class HomeViewState extends State<HomeView> {
_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<HomeView> {
);
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<void> _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<HomeView> {
_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<HomeView> {
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: <Widget>[
@ -281,16 +273,12 @@ class HomeViewState extends State<HomeView> {
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,
),
),
),

View file

@ -153,21 +153,19 @@ class _RegisterViewState extends State<RegisterView> {
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<RegisterView> {
),
),
),
const SizedBox(height: 48),
const SizedBox(height: 30),
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
@ -188,9 +186,7 @@ class _RegisterViewState extends State<RegisterView> {
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<RegisterView> {
),
),
),
if (_showUserNameError &&
usernameController.text.length < 3) ...[
if (_showUserNameError && usernameController.text.length < 3) ...[
const SizedBox(height: 8),
Text(
context.lang.registerUsernameLimits,

View file

@ -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(

View file

@ -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<PublicProfileView> {
Uint8List? _publicKey;
int _countContactRequest = 0;
late StreamSubscription<int?> _countContactRequestStream;
@override
void initState() {
@ -36,70 +33,15 @@ class _PublicProfileViewState extends State<PublicProfileView> {
Future<void> 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<PublicProfileView> {
),
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);
},

View file

@ -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),
),
],