new: screen lock

This commit is contained in:
otsmr 2026-04-10 16:34:16 +02:00
parent f419b3709d
commit 391646d243
15 changed files with 257 additions and 15 deletions

View file

@ -2,6 +2,7 @@
## 0.1.4 ## 0.1.4
- New: Screen lock for twonly (Can be enabled in the settings.)
- Fix: Several minor issues with the user interface - Fix: Several minor issues with the user interface
## 0.1.3 ## 0.1.3

View file

@ -19,6 +19,7 @@ import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart'; import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:twonly/src/views/settings/backup/setup_backup.view.dart'; import 'package:twonly/src/views/settings/backup/setup_backup.view.dart';
import 'package:twonly/src/views/unlock_twonly.view.dart';
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key}); const App({super.key});
@ -36,9 +37,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
globalCallbackConnectionState = ({required isConnected}) async { globalCallbackConnectionState = ({required isConnected}) async {
await context await context.read<CustomChangeProvider>().updateConnectionState(
.read<CustomChangeProvider>() isConnected,
.updateConnectionState(isConnected); );
await setUserPlan(); await setUserPlan();
}; };
@ -54,8 +55,8 @@ class _AppState extends State<App> with WidgetsBindingObserver {
if (user != null && mounted) { if (user != null && mounted) {
if (mounted) { if (mounted) {
context.read<PurchasesProvider>().updatePlan( context.read<PurchasesProvider>().updatePlan(
planFromString(user.subscriptionPlan), planFromString(user.subscriptionPlan),
); );
} }
} }
} }
@ -134,6 +135,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _showOnboarding = true; bool _showOnboarding = true;
bool _isLoaded = false; bool _isLoaded = false;
bool _skipBackup = false; bool _skipBackup = false;
bool _isTwonlyLocked = true;
int _initialPage = 0; int _initialPage = 0;
(Future<int>?, bool) _proofOfWork = (null, false); (Future<int>?, bool) _proofOfWork = (null, false);
@ -149,6 +151,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
_isUserCreated = await isUserCreated(); _isUserCreated = await isUserCreated();
if (_isUserCreated) { if (_isUserCreated) {
if (_isTwonlyLocked) {
// do not change in case twonly was already unlocked at some point
_isTwonlyLocked = gUser.screenLockEnabled;
}
if (gUser.appVersion < 62) { if (gUser.appVersion < 62) {
_showDatabaseMigration = true; _showDatabaseMigration = true;
} }
@ -164,8 +170,10 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (proof != null) { if (proof != null) {
Log.info('Starting with proof of work calculation.'); Log.info('Starting with proof of work calculation.');
// Starting with the proof of work. // Starting with the proof of work.
_proofOfWork = _proofOfWork = (
(calculatePoW(proof.prefix, proof.difficulty.toInt()), false); calculatePoW(proof.prefix, proof.difficulty.toInt()),
false,
);
} else { } else {
_proofOfWork = (null, disabled); _proofOfWork = (null, disabled);
} }
@ -187,7 +195,13 @@ class _AppMainWidgetState extends State<AppMainWidget> {
if (_showDatabaseMigration) { if (_showDatabaseMigration) {
child = const Center(child: Text('Please reinstall twonly.')); child = const Center(child: Text('Please reinstall twonly.'));
} else if (_isUserCreated) { } else if (_isUserCreated) {
if (gUser.twonlySafeBackup == null && !_skipBackup) { if (_isTwonlyLocked) {
child = UnlockTwonlyView(
callbackOnSuccess: () => setState(() {
_isTwonlyLocked = false;
}),
);
} else if (gUser.twonlySafeBackup == null && !_skipBackup) {
child = SetupBackupView( child = SetupBackupView(
callBack: () { callBack: () {
_skipBackup = true; _skipBackup = true;

View file

@ -3087,6 +3087,48 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Your QR code'** /// **'Your QR code'**
String get profileYourQrCode; String get profileYourQrCode;
/// No description provided for @settingsScreenLock.
///
/// In en, this message translates to:
/// **'Screen lock'**
String get settingsScreenLock;
/// No description provided for @settingsScreenLockSubtitle.
///
/// In en, this message translates to:
/// **'To open twonly, you\'ll need to use your smartphone\'s unlock feature.'**
String get settingsScreenLockSubtitle;
/// No description provided for @settingsScreenLockAuthMessageEnable.
///
/// In en, this message translates to:
/// **'Use the screen lock from twonly.'**
String get settingsScreenLockAuthMessageEnable;
/// No description provided for @settingsScreenLockAuthMessageDisable.
///
/// In en, this message translates to:
/// **'Disable the screen lock from twonly.'**
String get settingsScreenLockAuthMessageDisable;
/// No description provided for @unlockTwonly.
///
/// In en, this message translates to:
/// **'Unlock twonly'**
String get unlockTwonly;
/// No description provided for @unlockTwonlyTryAgain.
///
/// In en, this message translates to:
/// **'Try again'**
String get unlockTwonlyTryAgain;
/// No description provided for @unlockTwonlyDesc.
///
/// In en, this message translates to:
/// **'Use your phone\'s unlock settings to unlock twonly'**
String get unlockTwonlyDesc;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -1729,4 +1729,29 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Dein QR-Code'; String get profileYourQrCode => 'Dein QR-Code';
@override
String get settingsScreenLock => 'Bildschirmsperre';
@override
String get settingsScreenLockSubtitle =>
'Um twonly zu öffnen, wird die Entsperrfunktion deines Smartphones verwenden.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Bildschirmsperre von twonly verwenden';
@override
String get settingsScreenLockAuthMessageDisable =>
'Bildschirmsperre von twonly deaktivieren.';
@override
String get unlockTwonly => 'twonly entsperren';
@override
String get unlockTwonlyTryAgain => 'Erneut versuchen';
@override
String get unlockTwonlyDesc =>
'Entsperre twonly über die Sperreinstellungen deines Handys';
} }

View file

@ -1717,4 +1717,29 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Your QR code'; String get profileYourQrCode => 'Your QR code';
@override
String get settingsScreenLock => 'Screen lock';
@override
String get settingsScreenLockSubtitle =>
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Use the screen lock from twonly.';
@override
String get settingsScreenLockAuthMessageDisable =>
'Disable the screen lock from twonly.';
@override
String get unlockTwonly => 'Unlock twonly';
@override
String get unlockTwonlyTryAgain => 'Try again';
@override
String get unlockTwonlyDesc =>
'Use your phone\'s unlock settings to unlock twonly';
} }

View file

@ -1717,4 +1717,29 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get profileYourQrCode => 'Your QR code'; String get profileYourQrCode => 'Your QR code';
@override
String get settingsScreenLock => 'Screen lock';
@override
String get settingsScreenLockSubtitle =>
'To open twonly, you\'ll need to use your smartphone\'s unlock feature.';
@override
String get settingsScreenLockAuthMessageEnable =>
'Use the screen lock from twonly.';
@override
String get settingsScreenLockAuthMessageDisable =>
'Disable the screen lock from twonly.';
@override
String get unlockTwonly => 'Unlock twonly';
@override
String get unlockTwonlyTryAgain => 'Try again';
@override
String get unlockTwonlyDesc =>
'Use your phone\'s unlock settings to unlock twonly';
} }

View file

@ -87,6 +87,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false; bool allowErrorTrackingViaSentry = false;
@JsonKey(defaultValue: false)
bool screenLockEnabled = false;
// -- Custom DATA -- // -- Custom DATA --
@JsonKey(defaultValue: 100_000) @JsonKey(defaultValue: 100_000)

View file

@ -31,7 +31,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..videoStabilizationEnabled = ..videoStabilizationEnabled =
json['videoStabilizationEnabled'] as bool? ?? false json['videoStabilizationEnabled'] as bool? ?? true
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
..showShowImagePreviewWhenSending = ..showShowImagePreviewWhenSending =
json['showShowImagePreviewWhenSending'] as bool? ?? false json['showShowImagePreviewWhenSending'] as bool? ?? false
@ -62,6 +62,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..allowErrorTrackingViaSentry = ..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false json['allowErrorTrackingViaSentry'] as bool? ?? false
..screenLockEnabled = json['screenLockEnabled'] as bool? ?? false
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -123,6 +124,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated
?.toIso8601String(), ?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'screenLockEnabled': instance.screenLockEnabled,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; 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:go_router/go_router.dart';
@ -14,13 +15,13 @@ import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_messages_components/blink.component.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_group_action.dart';
import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/chat_list_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_date_chip.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_input.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';
import 'package:twonly/src/views/chats/chat_messages_components/blink.component.dart';
import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/flame.dart';
import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/components/verified_shield.dart';

View file

@ -17,10 +17,10 @@ import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_con
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_flame_restored.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_media_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_text_entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/chat_unknown.entry.dart';
import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart'; import 'package:twonly/src/views/chats/chat_messages_components/entries/common.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_reply_drag.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart'; import 'package:twonly/src/views/chats/chat_messages_components/message_context_menu.dart';
import 'package:twonly/src/views/chats/chat_messages_components/message_reply_drag.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart'; import 'package:twonly/src/views/chats/chat_messages_components/response_container.dart';
import 'package:twonly/src/views/components/avatar_icon.component.dart'; import 'package:twonly/src/views/components/avatar_icon.component.dart';

View file

@ -31,7 +31,7 @@ class _SlidingResponseWidgetState extends State<MessageReplyDrag> {
_animatedScale = 1.3; _animatedScale = 1.3;
}); });
await Future.delayed(Duration(milliseconds: 50)); await Future.delayed(const Duration(milliseconds: 50));
if (mounted) { if (mounted) {
setState(() { setState(() {

View file

@ -190,7 +190,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
hasLoader = true; hasLoader = true;
} }
if (message.mediaStored) { if (message.mediaStored && message.openedAt != null) {
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color); icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
text = context.lang.messageStoredInGallery; text = context.lang.messageStoredInGallery;
} }
@ -275,7 +275,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
), ),
]; ];
} }
// Log.info("DISPLAY REACTION");
} }
} }
if (widget.group != null && if (widget.group != null &&

View file

@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class PrivacyView extends StatefulWidget { class PrivacyView extends StatefulWidget {
const PrivacyView({super.key}); const PrivacyView({super.key});
@ -17,6 +18,20 @@ class _PrivacyViewState extends State<PrivacyView> {
super.initState(); super.initState();
} }
Future<void> toggleAuthRequirementOnStartup() async {
final isAuth = await authenticateUser(
gUser.screenLockEnabled
? context.lang.settingsScreenLockAuthMessageDisable
: context.lang.settingsScreenLockAuthMessageEnable,
);
if (!isAuth) return;
await updateUserdata((u) {
u.screenLockEnabled = !u.screenLockEnabled;
return u;
});
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -43,6 +58,15 @@ class _PrivacyViewState extends State<PrivacyView> {
), ),
onTap: () => context.push(Routes.settingsPrivacyBlockUsers), onTap: () => context.push(Routes.settingsPrivacyBlockUsers),
), ),
ListTile(
title: Text(context.lang.settingsScreenLock),
subtitle: Text(context.lang.settingsScreenLockSubtitle),
onTap: toggleAuthRequirementOnStartup,
trailing: Switch(
value: gUser.screenLockEnabled,
onChanged: (a) => toggleAuthRequirementOnStartup(),
),
),
ListTile( ListTile(
title: Text(context.lang.contactVerifyNumberTitle), title: Text(context.lang.contactVerifyNumberTitle),
onTap: () async { onTap: () async {

View file

@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/utils/misc.dart';
class UnlockTwonlyView extends StatefulWidget {
const UnlockTwonlyView({required this.callbackOnSuccess, super.key});
final void Function() callbackOnSuccess;
@override
State<UnlockTwonlyView> createState() => _UnlockTwonlyViewState();
}
class _UnlockTwonlyViewState extends State<UnlockTwonlyView> {
Future<void> _unlockTwonly() async {
final isAuth = await authenticateUser(context.lang.unlockTwonly);
if (isAuth) {
widget.callbackOnSuccess();
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// _unlockTwonly();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(),
const Icon(
FontAwesomeIcons.lock,
size: 40,
),
const SizedBox(height: 24),
Text(
context.lang.unlockTwonly,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 24),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(30),
child: Text(
context.lang.unlockTwonlyDesc,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
),
const SizedBox(height: 16),
Center(
child: FilledButton(
onPressed: _unlockTwonly,
child: Text(context.lang.unlockTwonlyTryAgain),
),
),
const SizedBox(height: 24),
],
),
),
),
);
}
}