diff --git a/CHANGELOG.md b/CHANGELOG.md index b08fdc0..c4d47b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/app.dart b/lib/app.dart index a60a3b2..7166d22 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -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 with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); globalCallbackConnectionState = ({required isConnected}) async { - await context - .read() - .updateConnectionState(isConnected); + await context.read().updateConnectionState( + isConnected, + ); await setUserPlan(); }; @@ -54,8 +55,8 @@ class _AppState extends State with WidgetsBindingObserver { if (user != null && mounted) { if (mounted) { context.read().updatePlan( - planFromString(user.subscriptionPlan), - ); + planFromString(user.subscriptionPlan), + ); } } } @@ -134,6 +135,7 @@ class _AppMainWidgetState extends State { bool _showOnboarding = true; bool _isLoaded = false; bool _skipBackup = false; + bool _isTwonlyLocked = true; int _initialPage = 0; (Future?, bool) _proofOfWork = (null, false); @@ -149,6 +151,10 @@ class _AppMainWidgetState extends State { _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 { 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 { 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; diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index a540115..4e9716b 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -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 diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index fc89712..a0dc6fc 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -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'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 93cf645..cf16f1f 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -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'; } diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index 230b7d9..0028018 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -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'; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 40dccb3..b00e1f9 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -87,6 +87,9 @@ class UserData { @JsonKey(defaultValue: false) bool allowErrorTrackingViaSentry = false; + @JsonKey(defaultValue: false) + bool screenLockEnabled = false; + // -- Custom DATA -- @JsonKey(defaultValue: 100_000) diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 1ce21d6..a6c20b0 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -31,7 +31,7 @@ UserData _$UserDataFromJson(Map 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 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 _$UserDataToJson(UserData instance) => { 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated ?.toIso8601String(), 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, + 'screenLockEnabled': instance.screenLockEnabled, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, diff --git a/lib/src/views/chats/chat_messages.view.dart b/lib/src/views/chats/chat_messages.view.dart index 562c3ee..da25040 100644 --- a/lib/src/views/chats/chat_messages.view.dart +++ b/lib/src/views/chats/chat_messages.view.dart @@ -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'; diff --git a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart index 9b48759..fa3a9f2 100644 --- a/lib/src/views/chats/chat_messages_components/chat_list_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_list_entry.dart @@ -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'; diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_unknown.entry.dart similarity index 100% rename from lib/src/views/chats/chat_messages_components/entries/chat_unkown.entry.dart rename to lib/src/views/chats/chat_messages_components/entries/chat_unknown.entry.dart diff --git a/lib/src/views/chats/chat_messages_components/message_reply_drag.dart b/lib/src/views/chats/chat_messages_components/message_reply_drag.dart index 4241f82..a2cd678 100644 --- a/lib/src/views/chats/chat_messages_components/message_reply_drag.dart +++ b/lib/src/views/chats/chat_messages_components/message_reply_drag.dart @@ -31,7 +31,7 @@ class _SlidingResponseWidgetState extends State { _animatedScale = 1.3; }); - await Future.delayed(Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 50)); if (mounted) { setState(() { diff --git a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart index 18e926a..8aa54fd 100644 --- a/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart +++ b/lib/src/views/chats/chat_messages_components/message_send_state_icon.dart @@ -190,7 +190,7 @@ class _MessageSendStateIconState extends State { 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 { ), ]; } - // Log.info("DISPLAY REACTION"); } } if (widget.group != null && diff --git a/lib/src/views/settings/privacy.view.dart b/lib/src/views/settings/privacy.view.dart index 976fe36..68e9aa9 100644 --- a/lib/src/views/settings/privacy.view.dart +++ b/lib/src/views/settings/privacy.view.dart @@ -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 { super.initState(); } + Future 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 { ), 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 { diff --git a/lib/src/views/unlock_twonly.view.dart b/lib/src/views/unlock_twonly.view.dart new file mode 100644 index 0000000..0e9a64d --- /dev/null +++ b/lib/src/views/unlock_twonly.view.dart @@ -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 createState() => _UnlockTwonlyViewState(); +} + +class _UnlockTwonlyViewState extends State { + Future _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), + ], + ), + ), + ), + ); + } +}