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

View file

@ -3087,6 +3087,48 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Your QR code'**
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

View file

@ -1729,4 +1729,29 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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
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
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)
bool allowErrorTrackingViaSentry = false;
@JsonKey(defaultValue: false)
bool screenLockEnabled = false;
// -- Custom DATA --
@JsonKey(defaultValue: 100_000)

View file

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

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/notifications/background.notifications.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_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/message_input.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/chats/chat_messages_components/blink.component.dart';
import 'package:twonly/src/views/components/flame.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_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_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/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_reply_drag.dart';
import 'package:twonly/src/views/chats/chat_messages_components/response_container.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;
});
await Future.delayed(Duration(milliseconds: 50));
await Future.delayed(const Duration(milliseconds: 50));
if (mounted) {
setState(() {

View file

@ -190,7 +190,7 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
hasLoader = true;
}
if (message.mediaStored) {
if (message.mediaStored && message.openedAt != null) {
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
text = context.lang.messageStoredInGallery;
}
@ -275,7 +275,6 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
),
];
}
// Log.info("DISPLAY REACTION");
}
}
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/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class PrivacyView extends StatefulWidget {
const PrivacyView({super.key});
@ -17,6 +18,20 @@ class _PrivacyViewState extends State<PrivacyView> {
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
Widget build(BuildContext context) {
return Scaffold(
@ -43,6 +58,15 @@ class _PrivacyViewState extends State<PrivacyView> {
),
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(
title: Text(context.lang.contactVerifyNumberTitle),
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),
],
),
),
),
);
}
}