improve setup

This commit is contained in:
otsmr 2026-04-29 15:48:52 +02:00
parent 836e58ec3a
commit c9b8e32d32
19 changed files with 474 additions and 422 deletions

View file

@ -155,7 +155,7 @@ Future<void> runMigrations() async {
if (u.avatarSvg == null) {
u.currentSetupPage = SetupPages.profile.name;
} else {
u.currentSetupPage = SetupPages.userDiscovery.name;
u.currentSetupPage = SetupPages.shareYourFriends.name;
}
});
}

View file

@ -2399,7 +2399,7 @@ abstract class AppLocalizations {
/// No description provided for @onboardingUserDiscoveryShareFriendsDesc.
///
/// In en, this message translates to:
/// **'Share with your friends who you know and who you have verified. Friends can *only see mutual friends* from your friend list.'**
/// **'Share with your friends who you know and who you have verified. Friends can *only see mutual friends* from your friend list. You can change your mind at *any time* or *hide specific people*.'**
String get onboardingUserDiscoveryShareFriendsDesc;
/// No description provided for @onboardingUserDiscoveryContactsVerifiedBadge.
@ -2429,7 +2429,7 @@ abstract class AppLocalizations {
/// No description provided for @userDiscoverySettingsManualApprovalDesc.
///
/// In en, this message translates to:
/// **'Before sharing someone, you will be asked every time someone reaches the number of send images.'**
/// **'Before someone is shared, you\'ll be asked first.'**
String get userDiscoverySettingsManualApprovalDesc;
/// No description provided for @onboardingUserDiscoveryLetFriendsFindYou.
@ -2989,6 +2989,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Share contact'**
String get userDiscoveryManualApprovalShareContact;
/// No description provided for @onboardingSetupCompleteTitle.
///
/// In en, this message translates to:
/// **'You\'re all set, {username}!'**
String onboardingSetupCompleteTitle(Object username);
/// No description provided for @onboardingSetupCompleteDesc.
///
/// In en, this message translates to:
/// **'You can now share your moments with your friends securely without distractions like ads.'**
String get onboardingSetupCompleteDesc;
}
class _AppLocalizationsDelegate

View file

@ -1328,7 +1328,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get userDiscoverySettingsManualApprovalDesc =>
'Bevor du jemanden teilst, wirst du jedes Mal gefragt, sobald jemand die Anzahl der gesendeten Bilder erreicht hat.';
'Bevor jemand geteilt wird, wirst du zuerst gefragt.';
@override
String get onboardingUserDiscoveryLetFriendsFindYou =>
@ -1683,4 +1683,13 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get userDiscoveryManualApprovalShareContact => 'Kontakt teilen';
@override
String onboardingSetupCompleteTitle(Object username) {
return 'Du bist startklar, $username!';
}
@override
String get onboardingSetupCompleteDesc =>
'Du kannst jetzt deine Momente sicher mit deinen Freunden teilen, ohne Ablenkungen wie Werbung.';
}

View file

@ -1300,7 +1300,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get onboardingUserDiscoveryShareFriendsDesc =>
'Share with your friends who you know and who you have verified. Friends can *only see mutual friends* from your friend list.';
'Share with your friends who you know and who you have verified. Friends can *only see mutual friends* from your friend list. You can change your mind at *any time* or *hide specific people*.';
@override
String get onboardingUserDiscoveryContactsVerifiedBadge =>
@ -1319,7 +1319,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get userDiscoverySettingsManualApprovalDesc =>
'Before sharing someone, you will be asked every time someone reaches the number of send images.';
'Before someone is shared, you\'ll be asked first.';
@override
String get onboardingUserDiscoveryLetFriendsFindYou =>
@ -1667,4 +1667,13 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get userDiscoveryManualApprovalShareContact => 'Share contact';
@override
String onboardingSetupCompleteTitle(Object username) {
return 'You\'re all set, $username!';
}
@override
String get onboardingSetupCompleteDesc =>
'You can now share your moments with your friends securely without distractions like ads.';
}

@ -1 +1 @@
Subproject commit 492bef11cf6bd472a71bd1c1b3007d731adea433
Subproject commit 96fc996e06f96a7a26438bba0421cb26eb721427

View file

@ -48,10 +48,6 @@ class MainCameraPreview extends StatelessWidget {
mainCameraController.cameraController!,
child: Stack(
children: [
if (mainCameraController.customPaint != null)
Positioned.fill(
child: mainCameraController.customPaint!,
),
if (mainCameraController.facePaint != null)
Positioned.fill(
child: mainCameraController.facePaint!,
@ -64,8 +60,7 @@ class MainCameraPreview extends StatelessWidget {
),
),
),
if (mainCameraController.focusPointOffset != null &&
!mainCameraController.isSharePreviewIsShown)
if (!mainCameraController.isSharePreviewIsShown)
AspectRatio(
aspectRatio: 9 / 16,
child: ClipRect(
@ -84,10 +79,18 @@ class MainCameraPreview extends StatelessWidget {
.width,
child: Stack(
children: [
if (mainCameraController.qrCodePain != null)
Positioned.fill(
child: mainCameraController.qrCodePain!,
),
if (mainCameraController.focusPointOffset != null)
Positioned(
top: mainCameraController.focusPointOffset!.dy - 40,
top:
mainCameraController.focusPointOffset!.dy -
40,
left:
mainCameraController.focusPointOffset!.dx - 40,
mainCameraController.focusPointOffset!.dx -
40,
child: Container(
height: 80,
width: 80,

View file

@ -76,7 +76,7 @@ class MainCameraController {
);
bool _isBusy = false;
bool _isBusyFaces = false;
CustomPaint? customPaint;
CustomPaint? qrCodePain;
CustomPaint? facePaint;
Offset? focusPointOffset;
@ -183,7 +183,7 @@ class MainCameraController {
..cameraId = cameraId;
facePaint = null;
customPaint = null;
qrCodePain = null;
isSelectingFaceFilters = false;
setFilter(FaceFilterType.none);
zoomButtonKey = GlobalKey();
@ -334,7 +334,7 @@ class MainCameraController {
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
);
customPaint = CustomPaint(painter: painter);
qrCodePain = CustomPaint(painter: painter);
if (barcodes.isEmpty && timeSharedLinkWasSetWithQr != null) {
if (timeSharedLinkWasSetWithQr!.isAfter(

View file

@ -286,11 +286,18 @@ class _ContactViewState extends State<ContactView> {
..._transferredTrust.map(
(tt) => ListTile(
dense: true,
title: Text(
title: Row(
children: [
Text(
context.lang.contactVerifiedBy(
getContactDisplayName(tt.$1),
),
),
VerificationBadgeComp(
contact: tt.$1,
),
],
),
trailing: Text(
DateFormat.yMd(
Localizations.localeOf(context).toString(),

View file

@ -4,18 +4,21 @@ import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/onboarding/setup/add_new_contacts_setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/backup_setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/profile_setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/user_discovery_setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/verification_badge_setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/add_new_contacts.setup.dart';
import 'package:twonly/src/visual/views/onboarding/setup/backup.setup.dart';
import 'package:twonly/src/visual/views/onboarding/setup/let_your_friends_find_you.setup.dart';
import 'package:twonly/src/visual/views/onboarding/setup/profile.setup.dart';
import 'package:twonly/src/visual/views/onboarding/setup/share_your_friends.setup.dart';
import 'package:twonly/src/visual/views/onboarding/setup/verification_badge.setup.dart';
import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart';
enum SetupPages {
profile,
backup,
addNewContact,
verificationBadge,
userDiscovery,
shareYourFriends,
letYourFriendsFindYou,
}
extension SetupPagesExtension on SetupPages {
@ -28,7 +31,7 @@ extension SetupPagesExtension on SetupPages {
int get pageNumber => index + 1;
int get totalPages => SetupPages.values.length;
int get progressPercentage => (pageNumber / totalPages * 100).round();
int get progressPercentage => ((pageNumber - 1) / totalPages * 100).round();
String get progressText => '$pageNumber / $totalPages';
bool get isLast => index == SetupPages.values.length - 1;
@ -53,10 +56,13 @@ class SetupView extends StatefulWidget {
class _SetupViewState extends State<SetupView> {
StreamSubscription<void>? _userUpdateStream;
late UserDiscoverySetupState state;
@override
void initState() {
super.initState();
state = UserDiscoverySetupState(setState: setState);
if (widget.onUpdate != null) {
_userUpdateStream = userService.onUserUpdated.listen((u) {
if (userService.currentUser.currentSetupPage == null) {
@ -115,7 +121,7 @@ class _SetupViewState extends State<SetupView> {
key: ValueKey(currentPage.name),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
children: [
_buildPage(currentPage),
_buildPage(currentPage, state),
if (!currentPage.isLast)
SizedBox(
height: 50,
@ -145,7 +151,7 @@ class _SetupViewState extends State<SetupView> {
);
}
Widget _buildPage(SetupPages page) {
Widget _buildPage(SetupPages page, UserDiscoverySetupState state) {
switch (page) {
case SetupPages.profile:
return const ProfileSetupPage();
@ -155,8 +161,10 @@ class _SetupViewState extends State<SetupView> {
return const AddNewContactsPage();
case SetupPages.verificationBadge:
return const VerificationBadgeSetupPage();
case SetupPages.userDiscovery:
return const UserDiscoverySetupPage();
case SetupPages.shareYourFriends:
return ShareYourFriendsSetupPage(state: state);
case SetupPages.letYourFriendsFindYou:
return LetYourFriendsFindYou(state: state);
}
}
}

View file

@ -15,7 +15,9 @@ class _FinishSetupCompState extends State<FinishSetupComp> {
await context.navPush(
SetupView(
onUpdate: () {
if (mounted) {
Navigator.pop(context);
}
},
),
);

View file

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart';
class LetYourFriendsFindYou extends StatefulWidget {
const LetYourFriendsFindYou({required this.state, super.key});
final UserDiscoverySetupState state;
@override
State<LetYourFriendsFindYou> createState() => _LetYourFriendsFindYouState();
}
class _LetYourFriendsFindYouState extends State<LetYourFriendsFindYou> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (userService.currentUser.isUserDiscoveryEnabled &&
userService.currentUser.userDiscoverySharePromotion) {
// feature is already configured...
UserService.update((user) {
user.currentSetupPage = SetupPages.letYourFriendsFindYou.next()?.name;
});
}
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.state.isUserDiscoveryEnabled)
UserDiscoverySetupComp(
state: widget.state,
showOnlySpecificPage: UserDiscoveryPages.letYourFriendsFindYou,
)
else
Container(
padding: const EdgeInsets.all(32),
child: Column(
children: [
Lottie.asset(
'assets/animations/takephoto.lottie',
repeat: true,
height: 150,
),
const SizedBox(height: 60),
Text(
context.lang.onboardingSetupCompleteTitle(
userService.currentUser.username,
),
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
context.lang.onboardingSetupCompleteDesc,
style: const TextStyle(
fontSize: 16,
),
textAlign: TextAlign.center,
),
],
),
),
const SizedBox(height: 50),
NextButtonComp(
onPressed: () async {
return !(await widget.state.initializeOrUpdate());
},
),
],
),
);
}
}

View file

@ -1,30 +1,30 @@
import 'package:flutter/material.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
import 'package:twonly/src/visual/views/settings/privacy/user_discovery/components/user_discovery_setup.comp.dart';
class UserDiscoverySetupPage extends StatefulWidget {
const UserDiscoverySetupPage({super.key});
class ShareYourFriendsSetupPage extends StatefulWidget {
const ShareYourFriendsSetupPage({required this.state, super.key});
final UserDiscoverySetupState state;
@override
State<UserDiscoverySetupPage> createState() => _UserDiscoverySetupPageState();
State<ShareYourFriendsSetupPage> createState() =>
_ShareYourFriendsSetupPageState();
}
class _UserDiscoverySetupPageState extends State<UserDiscoverySetupPage> {
late UserDiscoverySetupState state;
class _ShareYourFriendsSetupPageState extends State<ShareYourFriendsSetupPage> {
@override
void initState() {
super.initState();
state = UserDiscoverySetupState(setState: setState);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (userService.currentUser.isUserDiscoveryEnabled) {
// feature is already configured...
UserService.update((user) {
user.currentSetupPage = SetupPages.userDiscovery.next()?.name;
user.currentSetupPage = SetupPages.shareYourFriends.next()?.name;
});
}
});
@ -36,17 +36,15 @@ class _UserDiscoverySetupPageState extends State<UserDiscoverySetupPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
context.lang.onboardingUserDiscoveryShareFriends,
style: Theme.of(context).textTheme.headlineSmall,
UserDiscoverySetupComp(
state: widget.state,
showOnlySpecificPage: UserDiscoveryPages.shareYourFriends,
),
const SizedBox(height: 32),
UserDiscoverySetupComp(state: state),
const SizedBox(height: 60),
NextButtonComp(
onPressed: () async {
return !(await state.initializeOrUpdate());
},
const NextButtonComp(
// onPressed: () async {
// return !(await widget.state.initializeOrUpdate());
// },
),
],
),

View file

@ -14,6 +14,7 @@ import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/visual/components/alert.dialog.dart';
import 'package:twonly/src/visual/views/onboarding/setup.view.dart';
import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart';
class DeveloperSettingsView extends StatefulWidget {
@ -131,6 +132,16 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
onTap: () =>
context.push(Routes.settingsDeveloperAutomatedTesting),
),
ListTile(
title: const Text('Reopen Setup'),
onTap: () async {
await UserService.update((u) {
u
..currentSetupPage = SetupPages.profile.name
..isUserDiscoveryEnabled = false;
});
},
),
],
);
},

View file

@ -11,15 +11,16 @@ import 'package:twonly/src/visual/views/onboarding/setup/components/setup_switch
const exampleUsers = [
'james',
'john',
'robert',
'michael',
'william',
'david',
'mary',
'john',
'patricia',
'robert',
'jennifer',
'michael',
'linda',
'william',
'lena',
'david',
];
class UserDiscoverySetupState {
@ -27,7 +28,6 @@ class UserDiscoverySetupState {
required this.setState,
this.isUserDiscoveryEnabled = true,
this.sharePromotion = true,
this.isShareAllContacts = false,
this.isManualApprovalEnabled = false,
this.threshold = 2,
this.requiredSendImages = 4,
@ -39,7 +39,6 @@ class UserDiscoverySetupState {
int threshold;
bool sharePromotion;
bool isShareAllContacts;
bool isManualApprovalEnabled;
int requiredSendImages;
@ -53,11 +52,6 @@ class UserDiscoverySetupState {
}
Future<bool> initializeOrUpdate() async {
if (isShareAllContacts) {
requiredSendImages = 0;
isManualApprovalEnabled = false;
}
if (isUserDiscoveryEnabled) {
await UserDiscoveryService.initializeOrUpdate(
threshold: threshold,
@ -76,13 +70,17 @@ class UserDiscoverySetupState {
}
}
enum UserDiscoveryPages { all, shareYourFriends, letYourFriendsFindYou }
class UserDiscoverySetupComp extends StatelessWidget {
const UserDiscoverySetupComp({
required this.state,
this.showOnlySpecificPage = UserDiscoveryPages.all,
super.key,
});
final UserDiscoverySetupState state;
final UserDiscoveryPages showOnlySpecificPage;
@override
Widget build(BuildContext context) {
@ -90,28 +88,13 @@ class UserDiscoverySetupComp extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
RichText(
text: TextSpan(
children: formattedText(
context,
context.lang.userDiscoveryDisabledIntro,
),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 80),
if (showOnlySpecificPage == UserDiscoveryPages.all ||
showOnlySpecificPage == UserDiscoveryPages.shareYourFriends) ...[
Text(
context.lang.onboardingUserDiscoveryIncreaseTrust,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
context.lang.onboardingUserDiscoveryShareFriends,
style: Theme.of(context).textTheme.headlineSmall,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const SizedBox(height: 32),
RichText(
text: TextSpan(
@ -123,7 +106,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
const SizedBox(height: 32),
SetupSwitchCard(
value: state.isUserDiscoveryEnabled,
@ -137,6 +120,31 @@ class UserDiscoverySetupComp extends StatelessWidget {
expandedChild: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SwitchListTile(
value: state.isManualApprovalEnabled,
onChanged: (val) => state.update(
() => state.isManualApprovalEnabled = val,
),
title: Text(
context.lang.userDiscoverySettingsManualApproval,
style: const TextStyle(fontSize: 13),
),
subtitle: Text(
context.lang.userDiscoverySettingsManualApprovalDesc,
style: const TextStyle(fontSize: 10),
),
tileColor: context.color.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Divider(),
),
Text(
context.lang.onboardingUserDiscoveryContactsVerifiedBadge,
style: TextStyle(
@ -237,132 +245,30 @@ class UserDiscoverySetupComp extends StatelessWidget {
),
),
if (showOnlySpecificPage == UserDiscoveryPages.all)
const SizedBox(height: 80),
Text(
context.lang.userDiscoveryDisabledYouHaveControl,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
RichText(
text: TextSpan(
children: formattedText(
context,
context.lang.userDiscoveryDisabledDecide,
),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
if (state.isUserDiscoveryEnabled)
Container(
decoration: BoxDecoration(
color: context.color.surfaceContainerLow,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SwitchListTile(
value: state.isShareAllContacts,
onChanged: (val) => state.update(() {
state.isShareAllContacts = val;
}),
title: Text(
context.lang.userDiscoverySettingsEnableAllContacts,
style: const TextStyle(fontSize: 13),
),
tileColor: context.color.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
),
if (!state.isShareAllContacts)
ListTile(
title: Text(
context.lang.userDiscoverySettingsMinImagesTitle,
style: const TextStyle(fontSize: 13),
),
trailing: DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: state.requiredSendImages,
items: List.generate(
9,
(index) {
final value = index + 2;
return DropdownMenuItem<int>(
value: value,
child: Text('$value'),
);
},
),
onChanged: (newValue) {
if (newValue != null) {
state.update(
() => state.requiredSendImages = newValue,
);
}
},
),
),
),
if (!state.isShareAllContacts)
SwitchListTile(
value: state.isManualApprovalEnabled,
onChanged: (val) => state.update(
() => state.isManualApprovalEnabled = val,
),
title: Text(
context.lang.userDiscoverySettingsManualApproval,
style: const TextStyle(fontSize: 13),
),
subtitle: Text(
context.lang.userDiscoverySettingsManualApprovalDesc,
style: const TextStyle(fontSize: 10),
),
tileColor: context.color.surfaceContainerLow,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
),
],
),
),
const SizedBox(height: 80),
if (showOnlySpecificPage == UserDiscoveryPages.all ||
showOnlySpecificPage ==
UserDiscoveryPages.letYourFriendsFindYou) ...[
Text(
context.lang.onboardingUserDiscoveryLetFriendsFindYou,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
style: Theme.of(context).textTheme.headlineSmall,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
const SizedBox(height: 32),
RichText(
text: TextSpan(
children: formattedText(
context,
context.lang.onboardingUserDiscoveryLetFriendsFindYouDesc,
context.lang.userDiscoveryDisabledIntro,
),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
const SizedBox(height: 32),
SetupSwitchCard(
value: state.sharePromotion,
@ -386,7 +292,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
style: const TextStyle(fontSize: 12),
),
),
const SizedBox(width: 6),
const SizedBox(width: 12),
DropdownButtonHideUnderline(
child: DropdownButton<int>(
value: state.threshold,
@ -529,6 +435,7 @@ class UserDiscoverySetupComp extends StatelessWidget {
),
),
],
],
),
);
}

View file

@ -25,8 +25,6 @@ class _UserDiscoverySettingsViewState extends State<UserDiscoverySettingsView> {
requiredSendImages: u.requiredSendImages,
isUserDiscoveryEnabled: u.isUserDiscoveryEnabled,
sharePromotion: u.userDiscoverySharePromotion,
isShareAllContacts:
u.requiredSendImages == 0 && !u.userDiscoveryRequiresManualApproval,
isManualApprovalEnabled: u.userDiscoveryRequiresManualApproval,
threshold: u.userDiscoveryThreshold,
);