From f8242b003d1871f454d16666d53b95e3816a096e Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 13 Feb 2026 02:25:08 +0100 Subject: [PATCH 01/13] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 1a48e4e..0b269c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.91+91 +version: 0.0.92+92 environment: sdk: ^3.6.0 From 09afd3dda0670fde6e69bbd0aefc183bff07945f Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 20 Feb 2026 22:52:36 +0100 Subject: [PATCH 02/13] fix: allow plus plan to restore flames --- lib/src/services/subscription.service.dart | 16 ++++++++++++++-- .../views/components/max_flame_list_title.dart | 2 +- test/features/premium_features.dart | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 test/features/premium_features.dart diff --git a/lib/src/services/subscription.service.dart b/lib/src/services/subscription.service.dart index c36797a..27b50cc 100644 --- a/lib/src/services/subscription.service.dart +++ b/lib/src/services/subscription.service.dart @@ -10,8 +10,20 @@ enum SubscriptionPlan { Plus, } -bool isAdditionalAccount(SubscriptionPlan plan) { - return plan == SubscriptionPlan.Free || plan == SubscriptionPlan.Plus; +enum PremiumFeatures { RestoreFlames } + +const Map> planPermissions = { + PremiumFeatures.RestoreFlames: [ + SubscriptionPlan.Family, + SubscriptionPlan.Plus, + SubscriptionPlan.Tester, + SubscriptionPlan.Pro, + ], +}; + +bool isUserAllowed(SubscriptionPlan plan, PremiumFeatures feature) { + final allowedPlans = planPermissions[feature] ?? []; + return allowedPlans.contains(plan); } bool isPayingUser(SubscriptionPlan plan) { diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index adddb1f..6b4800e 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -46,7 +46,7 @@ class _MaxFlameListTitleState extends State { } Future _restoreFlames() async { - if (!isPayingUser(getCurrentPlan())) { + if (!isUserAllowed(getCurrentPlan(), PremiumFeatures.RestoreFlames)) { await context.push(Routes.settingsSubscription); return; } diff --git a/test/features/premium_features.dart b/test/features/premium_features.dart new file mode 100644 index 0000000..43dda71 --- /dev/null +++ b/test/features/premium_features.dart @@ -0,0 +1,17 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/src/services/subscription.service.dart'; + +void main() { + group('testing subscription permissions', () { + test('test if restore flames is allowed', () { + expect( + true, + isUserAllowed(SubscriptionPlan.Plus, PremiumFeatures.RestoreFlames), + ); + expect( + false, + isUserAllowed(SubscriptionPlan.Free, PremiumFeatures.RestoreFlames), + ); + }); + }); +} From f2b95386c63e82fe87853aff09249b5b40661a8e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:01:25 +0100 Subject: [PATCH 03/13] feature: verification check mark --- assets/icons/verified_badge_green.svg | 4 ++ assets/icons/verified_badge_red.svg | 4 ++ assets/icons/verified_badge_yellow.svg | 4 ++ lib/src/constants/routes.keys.dart | 2 + .../generated/app_localizations.dart | 44 ++++++++++++- .../generated/app_localizations_de.dart | 28 ++++++++- .../generated/app_localizations_en.dart | 28 ++++++++- .../generated/app_localizations_sv.dart | 28 ++++++++- lib/src/localization/translations | 2 +- lib/src/providers/routing.provider.dart | 12 ++++ .../views/components/better_list_title.dart | 25 ++++---- lib/src/views/components/svg_icon.dart | 31 ++++++++++ lib/src/views/components/verified_shield.dart | 16 ++--- lib/src/views/contact/contact.view.dart | 23 ++++++- lib/src/views/public_profile.view.dart | 24 ++++++- .../views/settings/help/faq/verifybadge.dart | 62 +++++++++++++++++++ pubspec.yaml | 1 + 17 files changed, 308 insertions(+), 30 deletions(-) create mode 100644 assets/icons/verified_badge_green.svg create mode 100644 assets/icons/verified_badge_red.svg create mode 100644 assets/icons/verified_badge_yellow.svg create mode 100644 lib/src/views/components/svg_icon.dart create mode 100644 lib/src/views/settings/help/faq/verifybadge.dart diff --git a/assets/icons/verified_badge_green.svg b/assets/icons/verified_badge_green.svg new file mode 100644 index 0000000..98c69ac --- /dev/null +++ b/assets/icons/verified_badge_green.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/verified_badge_red.svg b/assets/icons/verified_badge_red.svg new file mode 100644 index 0000000..6e70a82 --- /dev/null +++ b/assets/icons/verified_badge_red.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/assets/icons/verified_badge_yellow.svg b/assets/icons/verified_badge_yellow.svg new file mode 100644 index 0000000..29ee80a --- /dev/null +++ b/assets/icons/verified_badge_yellow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index aa8bab5..29ed8af 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -39,6 +39,8 @@ class Routes { static const String settingsStorageExport = '/settings/storage_data/export'; static const String settingsHelp = '/settings/help'; static const String settingsHelpFaq = '/settings/help/faq'; + static const String settingsHelpFaqVerifyBadge = + '/settings/help/faq/verifybadge'; static const String settingsHelpContactUs = '/settings/help/contact_us'; static const String settingsHelpDiagnostics = '/settings/help/diagnostics'; static const String settingsHelpUserStudy = '/settings/help/user_study'; diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 1821ee6..934c86e 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -943,7 +943,7 @@ abstract class AppLocalizations { /// No description provided for @contactVerifyNumberTitle. /// /// In en, this message translates to: - /// **'Verify safety number'** + /// **'Verify contact'** String get contactVerifyNumberTitle; /// No description provided for @contactVerifyNumberTapToScan. @@ -970,6 +970,12 @@ abstract class AppLocalizations { /// **'To verify the end-to-end encryption with {username}, compare the numbers with their device. The person can also scan your code with their device.'** String contactVerifyNumberLongDesc(Object username); + /// No description provided for @contactViewMessage. + /// + /// In en, this message translates to: + /// **'Message'** + String get contactViewMessage; + /// No description provided for @contactNickname. /// /// In en, this message translates to: @@ -2991,6 +2997,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'To see this message, you need to update twonly.'** String get updateTwonlyMessage; + + /// No description provided for @verificationBadgeNote. + /// + /// In en, this message translates to: + /// **'You can verify your friends by scanning their public QR code. Click to learn more.'** + String get verificationBadgeNote; + + /// No description provided for @verificationBadgeTitle. + /// + /// In en, this message translates to: + /// **'Verification'** + String get verificationBadgeTitle; + + /// No description provided for @verificationBadgeGeneralDesc. + /// + /// In en, this message translates to: + /// **'The green checkmark gives you the certainty that you are messaging the right person.'** + String get verificationBadgeGeneralDesc; + + /// No description provided for @verificationBadgeGreenDesc. + /// + /// In en, this message translates to: + /// **'Contact that you have personally verified via QR code. This also verified their public key.'** + String get verificationBadgeGreenDesc; + + /// No description provided for @verificationBadgeYellowDesc. + /// + /// In en, this message translates to: + /// **'(Coming soon) Contact whose QR code was scanned by one of your personally verified contacts.'** + String get verificationBadgeYellowDesc; + + /// No description provided for @verificationBadgeRedDesc. + /// + /// In en, this message translates to: + /// **'Unknown contact whose identity has not yet been verified.'** + String get verificationBadgeRedDesc; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 4385fee..1ad8949 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -468,7 +468,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Dein Konto wird gelöscht. Es gibt keine Möglichkeit, es wiederherzustellen.'; @override - String get contactVerifyNumberTitle => 'Sicherheitsnummer verifizieren'; + String get contactVerifyNumberTitle => 'Benutzer verifizieren'; @override String get contactVerifyNumberTapToScan => 'Zum Scannen tippen'; @@ -484,6 +484,9 @@ class AppLocalizationsDe extends AppLocalizations { return 'Um die Ende-zu-Ende-Verschlüsselung mit $username zu verifizieren, vergleiche die Zahlen mit ihrem Gerät. Die Person kann auch deinen Code mit ihrem Gerät scannen.'; } + @override + String get contactViewMessage => 'Nachricht'; + @override String get contactNickname => 'Spitzname'; @@ -1669,4 +1672,27 @@ class AppLocalizationsDe extends AppLocalizations { @override String get updateTwonlyMessage => 'Um diese Nachricht zu sehen, musst du twonly aktualisieren.'; + + @override + String get verificationBadgeNote => + 'Du kannst deine Freunde verifizieren, indem du deren öffentlichen QR-Code scannst. Klicke, um mehr zu erfahren.'; + + @override + String get verificationBadgeTitle => 'Verifizierung'; + + @override + String get verificationBadgeGeneralDesc => + 'Der grüne Haken gibt dir die Sicherheit, dass du mit der richtigen Person schreibst.'; + + @override + String get verificationBadgeGreenDesc => + 'Kontakt, den du durch den QR-Code persönlich verifiziert hast. Dadurch wurde auch sein öffentlicher Schlüssel überprüft.'; + + @override + String get verificationBadgeYellowDesc => + '(Coming soon) Kontakt, dessen QR-Code von einem deiner persönlich verifizierten Kontakte gescannt wurde.'; + + @override + String get verificationBadgeRedDesc => + 'Unbekannter Kontakt, dessen Identität bisher nicht verifiziert wurde.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 62fee3d..3513748 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -463,7 +463,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Your account will be deleted. There is no change to restore it.'; @override - String get contactVerifyNumberTitle => 'Verify safety number'; + String get contactVerifyNumberTitle => 'Verify contact'; @override String get contactVerifyNumberTapToScan => 'Tap to scan'; @@ -479,6 +479,9 @@ class AppLocalizationsEn extends AppLocalizations { return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.'; } + @override + String get contactViewMessage => 'Message'; + @override String get contactNickname => 'Nickname'; @@ -1657,4 +1660,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get updateTwonlyMessage => 'To see this message, you need to update twonly.'; + + @override + String get verificationBadgeNote => + 'You can verify your friends by scanning their public QR code. Click to learn more.'; + + @override + String get verificationBadgeTitle => 'Verification'; + + @override + String get verificationBadgeGeneralDesc => + 'The green checkmark gives you the certainty that you are messaging the right person.'; + + @override + String get verificationBadgeGreenDesc => + 'Contact that you have personally verified via QR code. This also verified their public key.'; + + @override + String get verificationBadgeYellowDesc => + '(Coming soon) Contact whose QR code was scanned by one of your personally verified contacts.'; + + @override + String get verificationBadgeRedDesc => + 'Unknown contact whose identity has not yet been verified.'; } diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index 286fb3b..a32c895 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -463,7 +463,7 @@ class AppLocalizationsSv extends AppLocalizations { 'Your account will be deleted. There is no change to restore it.'; @override - String get contactVerifyNumberTitle => 'Verify safety number'; + String get contactVerifyNumberTitle => 'Verify contact'; @override String get contactVerifyNumberTapToScan => 'Tap to scan'; @@ -479,6 +479,9 @@ class AppLocalizationsSv extends AppLocalizations { return 'To verify the end-to-end encryption with $username, compare the numbers with their device. The person can also scan your code with their device.'; } + @override + String get contactViewMessage => 'Message'; + @override String get contactNickname => 'Nickname'; @@ -1657,4 +1660,27 @@ class AppLocalizationsSv extends AppLocalizations { @override String get updateTwonlyMessage => 'To see this message, you need to update twonly.'; + + @override + String get verificationBadgeNote => + 'You can verify your friends by scanning their public QR code. Click to learn more.'; + + @override + String get verificationBadgeTitle => 'Verification'; + + @override + String get verificationBadgeGeneralDesc => + 'The green checkmark gives you the certainty that you are messaging the right person.'; + + @override + String get verificationBadgeGreenDesc => + 'Contact that you have personally verified via QR code. This also verified their public key.'; + + @override + String get verificationBadgeYellowDesc => + '(Coming soon) Contact whose QR code was scanned by one of your personally verified contacts.'; + + @override + String get verificationBadgeRedDesc => + 'Unknown contact whose identity has not yet been verified.'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 69d295d..6147155 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 69d295db737253e0c1b68aedc39bf757e8d642e6 +Subproject commit 6147155ce50caa97864d56e42e49a6f54702785d diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 395a7fb..87f7c4b 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -32,6 +32,7 @@ import 'package:twonly/src/views/settings/help/contact_us.view.dart'; import 'package:twonly/src/views/settings/help/credits.view.dart'; import 'package:twonly/src/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/views/settings/help/faq.view.dart'; +import 'package:twonly/src/views/settings/help/faq/verifybadge.dart'; import 'package:twonly/src/views/settings/help/help.view.dart'; import 'package:twonly/src/views/settings/notification.view.dart'; import 'package:twonly/src/views/settings/privacy.view.dart'; @@ -227,6 +228,12 @@ final routerProvider = GoRouter( GoRoute( path: 'faq', builder: (context, state) => const FaqView(), + routes: [ + GoRoute( + path: 'verifybadge', + builder: (context, state) => const VerificationBadeFaqView(), + ), + ], ), GoRoute( path: 'contact_us', @@ -281,5 +288,10 @@ final routerProvider = GoRouter( ), ], ), + // Fallback instead of showing a Page Not Found error redirect to home + GoRoute( + path: '/:path(.*)', + redirect: (context, state) => '/', + ), ], ); diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index fe408a5..fac2f5e 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -27,20 +27,17 @@ class BetterListTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - leading: Padding( - padding: (padding == null) - ? const EdgeInsets.only( - right: 10, - left: 19, - ) - : padding!, - child: (leading != null) - ? leading - : FaIcon( - icon, - size: iconSize, - color: color, - ), + leading: SizedBox( + width: 50, + child: Center( + child: (leading != null) + ? leading + : FaIcon( + icon, + size: iconSize, + color: color, + ), + ), ), trailing: trailing, title: Text( diff --git a/lib/src/views/components/svg_icon.dart b/lib/src/views/components/svg_icon.dart new file mode 100644 index 0000000..efd067a --- /dev/null +++ b/lib/src/views/components/svg_icon.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class SvgIcons { + static const String verifiedGreen = 'assets/icons/verified_badge_green.svg'; + static const String verifiedYellow = 'assets/icons/verified_badge_yellow.svg'; + static const String verifiedRed = 'assets/icons/verified_badge_red.svg'; +} + +class SvgIcon extends StatelessWidget { + const SvgIcon({ + required this.assetPath, + super.key, + this.size = 24.0, + this.color, + }); + final String assetPath; + final double? size; + final Color? color; + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + assetPath, + width: size, + height: size, + colorFilter: + color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null, + ); + } +} diff --git a/lib/src/views/components/verified_shield.dart b/lib/src/views/components/verified_shield.dart index 73b6ab2..a18684d 100644 --- a/lib/src/views/components/verified_shield.dart +++ b/lib/src/views/components/verified_shield.dart @@ -1,17 +1,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/views/components/svg_icon.dart'; class VerifiedShield extends StatefulWidget { const VerifiedShield({ this.contact, this.group, super.key, - this.size = 18, + this.size = 15, }); final Group? group; final Contact? contact; @@ -64,11 +64,13 @@ class _VerifiedShieldState extends State { message: isVerified ? 'You verified this contact' : 'You have not verifies this contact.', - child: FaIcon( - isVerified ? FontAwesomeIcons.shieldHeart : Icons.gpp_maybe_rounded, - color: - isVerified ? Theme.of(context).colorScheme.primary : Colors.red, - size: widget.size, + child: Padding( + padding: const EdgeInsetsGeometry.only(top: 2), + child: SvgIcon( + assetPath: + isVerified ? SvgIcons.verifiedGreen : SvgIcons.verifiedRed, + size: widget.size, + ), ), ), ); diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 41bc54b..b0fc162 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -14,6 +14,7 @@ import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/views/components/flame.dart'; import 'package:twonly/src/views/components/max_flame_list_title.dart'; import 'package:twonly/src/views/components/select_chat_deletion_time.comp.dart'; +import 'package:twonly/src/views/components/svg_icon.dart'; import 'package:twonly/src/views/components/verified_shield.dart'; import 'package:twonly/src/views/groups/group.view.dart'; @@ -163,6 +164,21 @@ class _ContactViewState extends State { if (getContactDisplayName(contact) != contact.username) Center(child: Text('(${contact.username})')), const SizedBox(height: 50), + BetterListTile( + icon: FontAwesomeIcons.solidComments, + text: context.lang.contactViewMessage, + onTap: () async { + final group = + await twonlyDB.groupsDao.getDirectChat(contact.userId); + if (group != null && context.mounted) { + await context.push( + Routes.chatsMessages, + extra: group, + ); + } + }, + ), + const Divider(), BetterListTile( icon: FontAwesomeIcons.pencil, text: context.lang.contactNickname, @@ -176,7 +192,6 @@ class _ContactViewState extends State { } }, ), - const Divider(), SelectChatDeletionTimeListTitle( groupId: getUUIDforDirectChat(widget.userId, gUser.userId), ), @@ -185,7 +200,11 @@ class _ContactViewState extends State { contactId: widget.userId, ), BetterListTile( - icon: FontAwesomeIcons.shieldHeart, + leading: SvgIcon( + assetPath: SvgIcons.verifiedGreen, + size: 20, + color: IconTheme.of(context).color, + ), text: context.lang.contactVerifyNumberTitle, onTap: () async { await context.push(Routes.settingsPublicProfile); diff --git a/lib/src/views/public_profile.view.dart b/lib/src/views/public_profile.view.dart index e3a937b..a2bb355 100644 --- a/lib/src/views/public_profile.view.dart +++ b/lib/src/views/public_profile.view.dart @@ -45,9 +45,29 @@ class _PublicProfileViewState extends State { body: Column( children: [ Container(width: double.infinity), - const SizedBox( - height: 30, + const SizedBox(height: 10), + GestureDetector( + onTap: () => context.push(Routes.settingsHelpFaqVerifyBadge), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 30), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + decoration: BoxDecoration( + color: context.color.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Text(context.lang.verificationBadgeNote), + ), + const SizedBox(width: 10), + const FaIcon(FontAwesomeIcons.angleRight), + ], + ), + ), ), + const SizedBox(height: 20), if (_qrCode != null && _userAvatar != null) Container( decoration: BoxDecoration( diff --git a/lib/src/views/settings/help/faq/verifybadge.dart b/lib/src/views/settings/help/faq/verifybadge.dart new file mode 100644 index 0000000..f3efb3d --- /dev/null +++ b/lib/src/views/settings/help/faq/verifybadge.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/views/components/svg_icon.dart'; + +class VerificationBadeFaqView extends StatefulWidget { + const VerificationBadeFaqView({super.key}); + + @override + State createState() => + _VerificationBadeFaqViewState(); +} + +class _VerificationBadeFaqViewState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.verificationBadgeTitle), + ), + body: ListView( + padding: const EdgeInsets.all(40), + children: [ + Text( + context.lang.verificationBadgeGeneralDesc, + textAlign: TextAlign.center, + ), + const SizedBox(height: 40), + _buildItem( + icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40), + description: context.lang.verificationBadgeGreenDesc, + ), + _buildItem( + icon: const SvgIcon(assetPath: SvgIcons.verifiedYellow, size: 40), + description: context.lang.verificationBadgeYellowDesc, + ), + _buildItem( + icon: const SvgIcon(assetPath: SvgIcons.verifiedRed, size: 40), + description: context.lang.verificationBadgeRedDesc, + ), + ], + ), + ); + } + + Widget _buildItem({required Widget icon, required String description}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 40), + child: Row( + children: [ + icon, + const SizedBox(width: 20), + Expanded( + child: Text( + description, + style: const TextStyle(fontSize: 16, height: 1.4), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 0b269c2..2419c31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -203,6 +203,7 @@ flutter: assets: # Add assets from the images directory to the application. - assets/images/ + - assets/icons/ - assets/animated_icons/ - assets/animations/ - assets/passwords/ From 6da6f1a9bc6efd5eb4473ead2d0d0b81f540fd97 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:07:38 +0100 Subject: [PATCH 04/13] fix: increase emoji recent limit --- lib/src/views/chats/chat_messages_components/message_input.dart | 1 + lib/src/views/components/emoji_picker.bottom.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/src/views/chats/chat_messages_components/message_input.dart b/lib/src/views/chats/chat_messages_components/message_input.dart index af683a2..1f5aa53 100644 --- a/lib/src/views/chats/chat_messages_components/message_input.dart +++ b/lib/src/views/chats/chat_messages_components/message_input.dart @@ -509,6 +509,7 @@ class _MessageInputState extends State { TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), emojiViewConfig: EmojiViewConfig( backgroundColor: context.color.surfaceContainer, + recentsLimit: 40, ), searchViewConfig: SearchViewConfig( backgroundColor: context.color.surfaceContainer, diff --git a/lib/src/views/components/emoji_picker.bottom.dart b/lib/src/views/components/emoji_picker.bottom.dart index 6139966..2dff473 100755 --- a/lib/src/views/components/emoji_picker.bottom.dart +++ b/lib/src/views/components/emoji_picker.bottom.dart @@ -57,6 +57,7 @@ class EmojiPickerBottom extends StatelessWidget { TextStyle(fontSize: 24 * (Platform.isIOS ? 1.2 : 1)), emojiViewConfig: EmojiViewConfig( backgroundColor: context.color.surfaceContainer, + recentsLimit: 40, ), searchViewConfig: SearchViewConfig( backgroundColor: context.color.surfaceContainer, From 8ef497772a3a0baed8664d0f7bfe72711d579e03 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:07:54 +0100 Subject: [PATCH 05/13] fix: make contact shares click able if already added --- .../entries/chat_contacts.entry.dart | 86 +++++++++++-------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart b/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart index f025012..debc8f0 100644 --- a/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart +++ b/lib/src/views/chats/chat_messages_components/entries/chat_contacts.entry.dart @@ -3,7 +3,9 @@ import 'dart:convert'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; import 'package:twonly/globals.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart'; import 'package:twonly/src/services/api/utils.dart'; @@ -99,7 +101,15 @@ class _ContactRow extends StatefulWidget { class _ContactRowState extends State<_ContactRow> { bool _isLoading = false; - Future _onContactClick() async { + Future _onContactClick(bool isAdded) async { + if (widget.contact.userId.toInt() == gUser.userId) { + await context.push(Routes.settingsProfile); + return; + } + if (isAdded) { + await context.push(Routes.profileContact(widget.contact.userId.toInt())); + return; + } setState(() { _isLoading = true; }); @@ -152,46 +162,48 @@ class _ContactRowState extends State<_ContactRow> { widget.contact.userId.toInt() == gUser.userId; return GestureDetector( - onTap: (widget.message.senderId == null || isAdded || _isLoading) - ? null - : _onContactClick, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FaIcon( - FontAwesomeIcons.user, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 8), - Flexible( - child: BetterText( - text: widget.contact.displayName, - textColor: Colors.white, + onTap: _isLoading ? null : () => _onContactClick(isAdded), + child: ColoredBox( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon( + FontAwesomeIcons.user, + color: Colors.white, + size: 16, ), - ), - if (widget.message.senderId != null && !isAdded) ...[ - const Spacer(), const SizedBox(width: 8), - if (_isLoading) - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - else - const FaIcon( - FontAwesomeIcons.userPlus, - color: Colors.white, - size: 16, + Flexible( + child: BetterText( + text: widget.contact.displayName, + textColor: Colors.white, ), + ), + if (widget.message.senderId != null && !isAdded) ...[ + const Spacer(), + const SizedBox(width: 8), + if (_isLoading) + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), + ) + else + const FaIcon( + FontAwesomeIcons.userPlus, + color: Colors.white, + size: 16, + ), + ], ], - ], + ), ), ), ); From 2b3ee4fbb35e4173e6652385c57fd35617d83df6 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:17:51 +0100 Subject: [PATCH 06/13] fix: display the focus at least 500ms --- .../camera_preview_components/main_camera_controller.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart index da13414..a514b75 100644 --- a/lib/src/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/views/camera/camera_preview_components/main_camera_controller.dart @@ -207,6 +207,9 @@ class MainCameraController { Log.error(e); } + // display the focus point at least 500ms + await Future.delayed(const Duration(milliseconds: 500)); + focusPointOffset = null; setState(); } From 30db0a1ff59d5791a30266bcf5555168fc9af8d0 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:18:35 +0100 Subject: [PATCH 07/13] fix: quoted text message not shown properly --- lib/src/utils/misc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index e398fac..e7350fb 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -241,7 +241,7 @@ InputDecoration inputTextMessageDeco(BuildContext context) { String truncateString(String input, {int maxLength = 20}) { if (input.length > maxLength) { - return '${input.substring(0, maxLength)}...'; + return '${input.characters.take(maxLength)}...'; } return input; } From 15096056c084903bde42666e2ebbd0bd253b0e3e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:20:17 +0100 Subject: [PATCH 08/13] fix analyzer and add changelog --- CHANGELOG.md | 11 +++++++++++ analysis_options.yaml | 1 + lib/src/services/api/messages.dart | 2 ++ lib/src/views/chats/media_viewer.view.dart | 1 - lib/src/views/chats/start_new_chat.view.dart | 2 ++ .../groups/group_create_select_members.view.dart | 2 ++ lib/src/views/memories/memories.view.dart | 2 ++ lib/src/views/settings/backup/backup_server.view.dart | 2 +- .../subscription/select_additional_users.view.dart | 2 ++ lib/src/views/shared/select_contacts.view.dart | 2 ++ 10 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2152468..6fb4980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog + +## 0.0.93 + +- Feature: Verification checkmark for friends +- Fix: Added contacts in contact sharing where not click able +- Fix: Restore flames as a plus user +- Fix: Route not found when sharing image +- Fix: Increase recent limit in emoji keyboard +- Fix: Increase show time of the focus indication +- Fix: Quoted text message not shown properly + ## 0.0.92 - Adds the option to share contacts diff --git a/analysis_options.yaml b/analysis_options.yaml index fc69522..4328836 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,7 @@ analyzer: avoid_positional_boolean_parameters: ignore inference_failure_on_collection_literal: ignore matching_super_parameters: ignore + parameter_assignments: ignore exclude: - "lib/src/model/protobuf/**" - "lib/src/model/protobuf/api/websocket/**" diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index dd83859..b4452eb 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -69,12 +69,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ try { if (receiptId == null && receipt == null) return null; if (receipt == null) { + // ignore: parameter_assignments receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); if (receipt == null) { Log.warn('Receipt not found.'); return null; } } + // ignore: parameter_assignments receiptId = receipt.receiptId; final contact = diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 43642af..0d6f132 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -131,7 +131,6 @@ class _MediaViewerViewState extends State { } setState(() {}); if (firstRun) { - // ignore: parameter_assignments firstRun = false; await loadCurrentMediaFile(); } diff --git a/lib/src/views/chats/start_new_chat.view.dart b/lib/src/views/chats/start_new_chat.view.dart index 09926fc..65d36b7 100644 --- a/lib/src/views/chats/start_new_chat.view.dart +++ b/lib/src/views/chats/start_new_chat.view.dart @@ -1,3 +1,5 @@ +// ignore_for_file: parameter_assignments + import 'dart:async'; import 'package:drift/drift.dart' hide Column; import 'package:flutter/material.dart'; diff --git a/lib/src/views/groups/group_create_select_members.view.dart b/lib/src/views/groups/group_create_select_members.view.dart index 4d1947e..b4d1912 100644 --- a/lib/src/views/groups/group_create_select_members.view.dart +++ b/lib/src/views/groups/group_create_select_members.view.dart @@ -1,3 +1,5 @@ +// ignore_for_file: parameter_assignments + import 'dart:async'; import 'dart:collection'; import 'package:flutter/material.dart'; diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 19357c9..3a43fd0 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -1,3 +1,5 @@ +// ignore_for_file: parameter_assignments + import 'dart:async'; import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; diff --git a/lib/src/views/settings/backup/backup_server.view.dart b/lib/src/views/settings/backup/backup_server.view.dart index 871ed6e..de78081 100644 --- a/lib/src/views/settings/backup/backup_server.view.dart +++ b/lib/src/views/settings/backup/backup_server.view.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_dynamic_calls +// ignore_for_file: parameter_assignments, avoid_dynamic_calls import 'dart:async'; import 'dart:convert'; diff --git a/lib/src/views/settings/subscription/select_additional_users.view.dart b/lib/src/views/settings/subscription/select_additional_users.view.dart index 2a6d6f1..f949513 100644 --- a/lib/src/views/settings/subscription/select_additional_users.view.dart +++ b/lib/src/views/settings/subscription/select_additional_users.view.dart @@ -1,3 +1,5 @@ +// ignore_for_file: parameter_assignments + import 'dart:async'; import 'dart:collection'; import 'package:flutter/material.dart'; diff --git a/lib/src/views/shared/select_contacts.view.dart b/lib/src/views/shared/select_contacts.view.dart index 6b5a989..c013a6c 100644 --- a/lib/src/views/shared/select_contacts.view.dart +++ b/lib/src/views/shared/select_contacts.view.dart @@ -1,3 +1,5 @@ +// ignore_for_file: parameter_assignments + import 'dart:async'; import 'dart:collection'; import 'package:flutter/material.dart'; From 3bb262036910e932fa3264fee244a3a643d92063 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:25:42 +0100 Subject: [PATCH 09/13] Fix: open chat after the image expires in case a draft message exists --- CHANGELOG.md | 1 + lib/src/views/chats/media_viewer.view.dart | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb4980..b976edc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Feature: Verification checkmark for friends - Fix: Added contacts in contact sharing where not click able +- Fix: Open chat after the image expires in case a draft message exists - Fix: Restore flames as a plus user - Fix: Route not found when sharing image - Fix: Increase recent limit in emoji keyboard diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 0d6f132..ba1842f 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -4,10 +4,12 @@ import 'package:clock/clock.dart'; import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:photo_view/photo_view.dart'; import 'package:twonly/globals.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' show DownloadState, MediaType; @@ -153,7 +155,16 @@ class _MediaViewerViewState extends State { progressTimer?.cancel(); if (allMediaFiles.isEmpty) { - Navigator.pop(context); + final group = await twonlyDB.groupsDao.getGroup(widget.group.groupId); + if (mounted) { + if (group != null && + group.draftMessage != null && + group.draftMessage != '') { + context.replace(Routes.chatsMessages, extra: group); + } else { + Navigator.pop(context); + } + } } else { await loadCurrentMediaFile(); } From bbe6954ca63cafa4d9dc081b9037cfbbb760d0d2 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:28:23 +0100 Subject: [PATCH 10/13] Fix: Push notification in groups when someone saves an image --- CHANGELOG.md | 1 + lib/src/services/notifications/pushkeys.notifications.dart | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b976edc..5277a1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Fix: Increase recent limit in emoji keyboard - Fix: Increase show time of the focus indication - Fix: Quoted text message not shown properly +- Fix: Push notification in groups when someone saves an image ## 0.0.92 diff --git a/lib/src/services/notifications/pushkeys.notifications.dart b/lib/src/services/notifications/pushkeys.notifications.dart index dd319b8..2df0f34 100644 --- a/lib/src/services/notifications/pushkeys.notifications.dart +++ b/lib/src/services/notifications/pushkeys.notifications.dart @@ -281,6 +281,13 @@ Future getPushNotificationFromEncryptedContent( } if (content.hasMediaUpdate()) { + final msg = await twonlyDB.messagesDao + .getMessageById(content.reaction.targetMessageId) + .getSingleOrNull(); + // These notifications should only be send to the original sender. + if (msg == null || msg.senderId == null || msg.senderId != toUserId) { + return null; + } switch (content.mediaUpdate.type) { case EncryptedContent_MediaUpdate_Type.REOPENED: kind = PushKind.reopenedMedia; From 80910c1ba0c94e6669d29a91f7eea64479c57ef3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:35:21 +0100 Subject: [PATCH 11/13] Fix: Dark mode in diagnostics view --- CHANGELOG.md | 3 +- lib/src/providers/purchases.provider.dart | 2 +- .../views/settings/help/diagnostics.view.dart | 58 ++----------------- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5277a1b..e2b4147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ## 0.0.93 - Feature: Verification checkmark for friends -- Fix: Added contacts in contact sharing where not click able +- Fix: Added contacts in contact sharing that were not clickable. - Fix: Open chat after the image expires in case a draft message exists - Fix: Restore flames as a plus user - Fix: Route not found when sharing image @@ -12,6 +12,7 @@ - Fix: Increase show time of the focus indication - Fix: Quoted text message not shown properly - Fix: Push notification in groups when someone saves an image +- Fix: Dark mode in diagnostics view ## 0.0.92 diff --git a/lib/src/providers/purchases.provider.dart b/lib/src/providers/purchases.provider.dart index 29ba6a8..3895c18 100644 --- a/lib/src/providers/purchases.provider.dart +++ b/lib/src/providers/purchases.provider.dart @@ -83,7 +83,7 @@ class PurchasesProvider with ChangeNotifier, DiagnosticableTreeMixin { if (user != null && isPayingUser(planFromString(user.subscriptionPlan))) { Log.info('Started IPA timer for verification.'); globalForceIpaCheck = Timer(const Duration(seconds: 5), () async { - Log.warn('Force Ipa check was not stopped. Requesting forced check...'); + Log.info('Force Ipa check was not stopped. Requesting forced check...'); await apiService.forceIpaCheck(); }); } diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart index 8b05bcf..a96cd1c 100644 --- a/lib/src/views/settings/help/diagnostics.view.dart +++ b/lib/src/views/settings/help/diagnostics.view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:share_plus/share_plus.dart'; +import 'package:go_router/go_router.dart'; +import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/components/loader.dart'; @@ -13,8 +13,6 @@ class DiagnosticsView extends StatefulWidget { } class _DiagnosticsViewState extends State { - final ScrollController _scrollController = ScrollController(); - String? _debugLogText; @override @@ -28,41 +26,6 @@ class _DiagnosticsViewState extends State { setState(() {}); } - Future _scrollToBottom() async { - // Assuming the button is at the bottom of the scroll view - await _scrollController.animateTo( - _scrollController.position.maxScrollExtent, // Scroll to the bottom - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - - Future _shareDebugLog() async { - if (_debugLogText == null) return; - final directory = await getApplicationSupportDirectory(); - final logFile = XFile('${directory.path}/app.log'); - - final params = ShareParams( - text: 'Debug log', - files: [logFile], - ); - - final result = await SharePlus.instance.share(params); - - if (result.status != ShareResultStatus.success) { - await Clipboard.setData( - ClipboardData(text: _debugLogText!), - ); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Log copied to clipboard!'), - ), - ); - } - } - } - Future _deleteDebugLog() async { if (await deleteLogFile()) { if (!mounted) return; @@ -100,13 +63,10 @@ class _DiagnosticsViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( - onPressed: _shareDebugLog, + onPressed: () => + context.push(Routes.settingsHelpContactUs), child: const Text('Share debug log'), ), - TextButton( - onPressed: _scrollToBottom, - child: const Text('Scroll to Bottom'), - ), TextButton( onPressed: _deleteDebugLog, child: const Text('Delete Log File'), @@ -186,7 +146,6 @@ class _LogViewerWidgetState extends State { selected: selected, onSelected: (_) => _setFilter(label), selectedColor: _colorForLevel(label).withAlpha(120), - backgroundColor: Colors.grey.shade200, ); } @@ -198,7 +157,7 @@ class _LogViewerWidgetState extends State { fontWeight: FontWeight.bold, fontFamily: 'monospace', ); - const msgStyle = TextStyle(color: Colors.black87, fontFamily: 'monospace'); + const msgStyle = TextStyle(fontFamily: 'monospace'); return TextSpan( children: [ @@ -249,12 +208,7 @@ class _LogViewerWidgetState extends State { ), Expanded( child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.grey.shade50, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey.shade300), - ), + padding: const EdgeInsets.only(left: 8), child: Scrollbar( controller: _controller, child: ListView.builder( From cf0132e75b6f304e01da9e60d5f4c0d2ac27b78c Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:41:07 +0100 Subject: [PATCH 12/13] improve logging --- lib/src/services/api.service.dart | 6 +++++- lib/src/services/api/mediafiles/download.service.dart | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 816f575..79c34b1 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -193,7 +193,11 @@ class ApiService { } Future _onError(dynamic e) async { - Log.warn('websocket error: $e'); + if (e.toString().contains('Failed host lookup')) { + Log.info('WebSocket connection failed: Host not reachable.'); + } else { + Log.warn('websocket error: $e'); + } await onClosed(); } diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 6782e3b..ef8d17a 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -37,6 +37,14 @@ Future canMediaFileBeDownloaded(MediaFile mediaFile) async { // If not delete the message as it can not be downloaded from the server anymore. if (messages.length != 1) { + if (messages.isEmpty) { + MediaFileService(mediaFile).fullMediaRemoval(); + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + Log.warn( + 'Media file which is in downloading status has not text message. Deleting media file. ${mediaFile.mediaId}.', + ); + return false; + } Log.warn( 'A media for download must have one original message, but it has ${messages.length}.', ); From 7c9c4896d44a5917c3d7893f9bb4aad902d08c30 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 21 Feb 2026 01:43:44 +0100 Subject: [PATCH 13/13] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2419c31..86dc666 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.92+92 +version: 0.0.93+93 environment: sdk: ^3.6.0