From a029d2f4d53d286f135396e92b7e9bc804baae4a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 24 Jan 2026 20:12:55 +0100 Subject: [PATCH 01/22] fix user study open again --- lib/src/views/user_study/user_study_questionnaire.view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/views/user_study/user_study_questionnaire.view.dart b/lib/src/views/user_study/user_study_questionnaire.view.dart index 7058c5c..8713323 100644 --- a/lib/src/views/user_study/user_study_questionnaire.view.dart +++ b/lib/src/views/user_study/user_study_questionnaire.view.dart @@ -51,7 +51,9 @@ class _UserStudyQuestionnaireState extends State { await updateUserdata((u) { // generate a random participants id to identify data send later while keeping the user anonym - u.userStudyParticipantsToken = getRandomString(25); + u + ..userStudyParticipantsToken = getRandomString(25) + ..askedForUserStudyPermission = true; return u; }); From 77166253ae71a0a8d58972dc21eed88e08e9087a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 12:39:53 +0100 Subject: [PATCH 02/22] fix #390 --- .../layers/background.layer.dart | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/src/views/camera/share_image_editor/layers/background.layer.dart b/lib/src/views/camera/share_image_editor/layers/background.layer.dart index ecd3ab7..ab0408f 100755 --- a/lib/src/views/camera/share_image_editor/layers/background.layer.dart +++ b/lib/src/views/camera/share_image_editor/layers/background.layer.dart @@ -33,7 +33,7 @@ class _BackgroundLayerState extends State { width: widget.layerData.image.width.toDouble(), height: widget.layerData.image.height.toDouble(), padding: EdgeInsets.zero, - color: Colors.green, + color: Colors.transparent, child: CustomPaint( painter: UiImagePainter(scImage.image!), ), @@ -47,16 +47,25 @@ class UiImagePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + final imageSize = Size(image.width.toDouble(), image.height.toDouble()); + + final sizes = applyBoxFit(BoxFit.contain, imageSize, size); + + final destRect = Alignment.center.inscribe( + sizes.destination, + Rect.fromLTWH(0, 0, size.width, size.height), + ); + canvas.drawImageRect( image, - Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), - Rect.fromLTWH(0, 0, size.width, size.height), + Rect.fromLTWH(0, 0, imageSize.width, imageSize.height), + destRect, Paint(), ); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return false; + bool shouldRepaint(covariant UiImagePainter oldDelegate) { + return image != oldDelegate.image; } } From 318bb72b642e17550be5f60ce742c73a302ecb07 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 12:40:00 +0100 Subject: [PATCH 03/22] fix timing issue --- lib/src/views/user_study/user_study_data_collection.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/views/user_study/user_study_data_collection.dart b/lib/src/views/user_study/user_study_data_collection.dart index c38f213..1b576ee 100644 --- a/lib/src/views/user_study/user_study_data_collection.dart +++ b/lib/src/views/user_study/user_study_data_collection.dart @@ -4,6 +4,7 @@ import 'package:http/http.dart' as http; import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/keyvalue.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; const userStudySurveyKey = 'user_study_survey'; @@ -34,9 +35,8 @@ Future handleUserStudyUpload() async { await KeyValueStore.delete(userStudySurveyKey); } - if (gUser.lastUserStudyDataUpload - ?.isAfter(DateTime.now().subtract(const Duration(days: 1))) ?? - false) { + if (gUser.lastUserStudyDataUpload != null && + isToday(gUser.lastUserStudyDataUpload!)) { // Only send updates once a day. // This enables to see if improvements to actually work. return; From 7ac10d8326bb33da67c9e31be100eb6306f58b29 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 13:08:42 +0100 Subject: [PATCH 04/22] fix #387 --- .../camera_preview_components/main_camera_controller.dart | 5 +++++ lib/src/views/camera/share_image_editor.view.dart | 3 +++ .../layers/link_preview/cards/mastodon.card.dart | 6 ++++-- .../layers/link_preview/cards/twitter.card.dart | 4 ++-- 4 files changed, 14 insertions(+), 4 deletions(-) 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 cfbef1a..7b32001 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 @@ -65,6 +65,11 @@ class MainCameraController { setState(); } + void onImageSend() { + scannedUrl = ''; + setState(); + } + final BarcodeScanner _barcodeScanner = BarcodeScanner(); final FaceDetector _faceDetector = FaceDetector( options: FaceDetectorOptions( diff --git a/lib/src/views/camera/share_image_editor.view.dart b/lib/src/views/camera/share_image_editor.view.dart index 164fcf2..d767466 100644 --- a/lib/src/views/camera/share_image_editor.view.dart +++ b/lib/src/views/camera/share_image_editor.view.dart @@ -424,6 +424,7 @@ class _ShareImageEditorView extends State { ), ) as bool?; if (wasSend != null && wasSend && mounted) { + widget.mainCameraController?.onImageSend(); Navigator.pop(context, true); } else { await videoController?.play(); @@ -591,6 +592,8 @@ class _ShareImageEditorView extends State { if (!context.mounted) return; + widget.mainCameraController?.onImageSend(); + // must be awaited so the widget for the screenshot is not already disposed when sending.. await storeImageAsOriginal(); diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart index f69d78e..1892ed4 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart @@ -48,7 +48,9 @@ class MastodonPostCard extends StatelessWidget { const SizedBox(height: 4), if (info.desc != null && info.desc != 'null') Text( - substringBy(info.desc!, 1000), + substringBy( + info.desc!.replaceAll('Attached: 1 image', '').trim(), + info.image == null ? 500 : 300), style: const TextStyle(color: Colors.white, fontSize: 14), ), if (info.image != null && info.image != 'null') @@ -57,7 +59,7 @@ class MastodonPostCard extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 250), + constraints: const BoxConstraints(maxHeight: 200), child: CachedNetworkImage( imageUrl: info.image!, fit: BoxFit.contain, diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart index 7216b3f..a822c59 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/twitter.card.dart @@ -55,7 +55,7 @@ class TwitterPostCard extends StatelessWidget { const SizedBox(height: 8), if (info.desc != null && info.desc != 'null') Text( - substringBy(info.desc!, 1000), + substringBy(info.desc!, info.image == null ? 500 : 300), style: const TextStyle( color: primaryText, fontSize: 15, @@ -73,7 +73,7 @@ class TwitterPostCard extends StatelessWidget { borderRadius: BorderRadius.circular(14), ), child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), + constraints: const BoxConstraints(maxHeight: 200), child: CachedNetworkImage( imageUrl: info.image!, fit: BoxFit.cover, From aee31f5b40bca3575abb6be9ab0464c91fe73884 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 13:17:56 +0100 Subject: [PATCH 05/22] fix increased twonly safe backup --- lib/main.dart | 1 + lib/src/database/daos/receipts.dao.dart | 12 ++++++++++++ lib/src/database/twonly.db.dart | 1 + 3 files changed, 14 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 2140a8d..2b06327 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -65,6 +65,7 @@ void main() async { twonlyDB = TwonlyDB(); await twonlyDB.messagesDao.purgeMessageTable(); + await twonlyDB.receiptsDao.purgeReceivedReceipts(); unawaited(MediaFileService.purgeTempFolder()); await initFileDownloader(); diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index 26a72e0..4b6a6a1 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -51,6 +51,18 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { .go(); } + Future purgeReceivedReceipts() async { + await (delete(receivedReceipts) + ..where( + (t) => (t.createdAt.isSmallerThanValue( + clock.now().subtract( + const Duration(days: 25), + ), + )), + )) + .go(); + } + Future insertReceipt(ReceiptsCompanion entry) async { try { var insertEntry = entry; diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 07ec778..1d27de6 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -166,6 +166,7 @@ class TwonlyDB extends _$TwonlyDB { )) .go(); await delete(receipts).go(); + await delete(receivedReceipts).go(); await update(contacts).write( const ContactsCompanion( avatarSvgCompressed: Value(null), From 935d8101de8d1cb3497bfe053ad3b91f787d8b74 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 13:25:35 +0100 Subject: [PATCH 06/22] fix link not getting deleted --- lib/src/views/home.view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 355d38e..f67c335 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -87,6 +87,7 @@ class HomeViewState extends State { if (offsetRatio == 1) { disableCameraTimer = Timer(const Duration(milliseconds: 500), () async { await _mainCameraController.closeCamera(); + _mainCameraController.sharedLinkForPreview = null; disableCameraTimer = null; }); } From 462dedc17d3d5ece645276d9381292ea90db33fd Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 13:26:18 +0100 Subject: [PATCH 07/22] bump version --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7427a5d..5ac7cf8 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.87+87 +version: 0.0.89+89 environment: sdk: ^3.6.0 From fb93d0c9ece8813024862a5ed78d942ac9a3e55b Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 13:32:28 +0100 Subject: [PATCH 08/22] fix deeplink --- android/app/src/main/AndroidManifest.xml | 1 - .../layers/link_preview/cards/mastodon.card.dart | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index deed6bf..7f54da8 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -37,7 +37,6 @@ - diff --git a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart index 1892ed4..3f0852c 100644 --- a/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart +++ b/lib/src/views/camera/share_image_editor/layers/link_preview/cards/mastodon.card.dart @@ -49,8 +49,9 @@ class MastodonPostCard extends StatelessWidget { if (info.desc != null && info.desc != 'null') Text( substringBy( - info.desc!.replaceAll('Attached: 1 image', '').trim(), - info.image == null ? 500 : 300), + info.desc!.replaceAll('Attached: 1 image', '').trim(), + info.image == null ? 500 : 300, + ), style: const TextStyle(color: Colors.white, fontSize: 14), ), if (info.image != null && info.image != 'null') From 42a81b9309352a9090674d81a9d7c58d47863885 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 25 Jan 2026 16:46:41 +0100 Subject: [PATCH 09/22] adds new filter --- assets/filters/beard_upper_lip_green.webp | Bin 0 -> 3990 bytes assets/filters/hat_black.webp | Bin 0 -> 4092 bytes .../face_filters.dart | 5 ++- .../main_camera_controller.dart | 33 ++++++++++-------- .../face_filters/beard_filter_painter.dart | 30 ++++++++++++---- 5 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 assets/filters/beard_upper_lip_green.webp create mode 100644 assets/filters/hat_black.webp diff --git a/assets/filters/beard_upper_lip_green.webp b/assets/filters/beard_upper_lip_green.webp new file mode 100644 index 0000000000000000000000000000000000000000..4046f6516fbf8f9fcdddde615784149d25588890 GIT binary patch literal 3990 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 literal 0 HcmV?d00001 diff --git a/lib/src/views/camera/camera_preview_components/face_filters.dart b/lib/src/views/camera/camera_preview_components/face_filters.dart index 8c880a7..4913ed4 100644 --- a/lib/src/views/camera/camera_preview_components/face_filters.dart +++ b/lib/src/views/camera/camera_preview_components/face_filters.dart @@ -5,6 +5,7 @@ import 'package:twonly/src/views/camera/camera_preview_components/painters/face_ enum FaceFilterType { none, dogBrown, + beardUpperLipGreen, beardUpperLip, } @@ -27,7 +28,9 @@ extension FaceFilterTypeExtension on FaceFilterType { case FaceFilterType.dogBrown: return DogFilterPainter.getPreview(); case FaceFilterType.beardUpperLip: - return BeardFilterPainter.getPreview(); + return BeardFilterPainter.getPreview(this); + case FaceFilterType.beardUpperLipGreen: + return BeardFilterPainter.getPreview(this); } } } 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 7b32001..dd5364f 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 @@ -397,20 +397,25 @@ class MainCameraController { cameraController != null) { if (faces.isNotEmpty) { CustomPainter? painter; - if (_currentFilterType == FaceFilterType.dogBrown) { - painter = DogFilterPainter( - faces, - inputImage.metadata!.size, - inputImage.metadata!.rotation, - cameraController!.description.lensDirection, - ); - } else if (_currentFilterType == FaceFilterType.beardUpperLip) { - painter = BeardFilterPainter( - faces, - inputImage.metadata!.size, - inputImage.metadata!.rotation, - cameraController!.description.lensDirection, - ); + switch (_currentFilterType) { + case FaceFilterType.dogBrown: + painter = DogFilterPainter( + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + cameraController!.description.lensDirection, + ); + case FaceFilterType.beardUpperLip: + case FaceFilterType.beardUpperLipGreen: + painter = BeardFilterPainter( + _currentFilterType, + faces, + inputImage.metadata!.size, + inputImage.metadata!.rotation, + cameraController!.description.lensDirection, + ); + case FaceFilterType.none: + break; } if (painter != null) { diff --git a/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart b/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart index 3477bb6..35a478d 100644 --- a/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart +++ b/lib/src/views/camera/camera_preview_components/painters/face_filters/beard_filter_painter.dart @@ -6,27 +6,45 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_mlkit_face_detection/google_mlkit_face_detection.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/views/camera/camera_preview_components/face_filters.dart'; import 'package:twonly/src/views/camera/camera_preview_components/painters/coordinates_translator.dart'; import 'package:twonly/src/views/camera/camera_preview_components/painters/face_filters/face_filter_painter.dart'; class BeardFilterPainter extends FaceFilterPainter { BeardFilterPainter( + FaceFilterType beardType, super.faces, super.imageSize, super.rotation, super.cameraLensDirection, ) { - _loadAssets(); + _loadAssets(beardType); } + static FaceFilterType? _lastLoadedBeardType; static ui.Image? _beardImage; static bool _loading = false; - static Future _loadAssets() async { - if (_loading || _beardImage != null) return; + static String getAssetPath(FaceFilterType beardType) { + switch (beardType) { + case FaceFilterType.beardUpperLip: + return 'assets/filters/beard_upper_lip.webp'; + case FaceFilterType.beardUpperLipGreen: + return 'assets/filters/beard_upper_lip_green.webp'; + case FaceFilterType.dogBrown: + case FaceFilterType.none: + return ''; + } + } + + static Future _loadAssets(FaceFilterType beardType) async { + if ((_loading || _beardImage != null) && + _lastLoadedBeardType == beardType) { + return; + } _loading = true; try { - _beardImage = await _loadImage('assets/filters/beard_upper_lip.webp'); + _beardImage = await _loadImage(getAssetPath(beardType)); } catch (e) { Log.error('Failed to load filter assets: $e'); } finally { @@ -161,12 +179,12 @@ class BeardFilterPainter extends FaceFilterPainter { ..restore(); } - static Widget getPreview() { + static Widget getPreview(FaceFilterType beardType) { return Preview( child: Padding( padding: const EdgeInsets.all(8), child: Image.asset( - 'assets/filters/beard_upper_lip.webp', + getAssetPath(beardType), fit: BoxFit.contain, ), ), From 111befb84fc845f3ad34e54b7e921a21985d1699 Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 26 Jan 2026 22:26:57 +0100 Subject: [PATCH 10/22] update sub repo --- .gitmodules | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 60363aa..849ee55 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,8 @@ [submodule "dependencies"] path = dependencies - url = https://github.com/twonlyapp/twonly-app-dependencies.git + # url = ssh://git@git.twonly.eu:22222/twonly/twonly-app-dependencies.git + url = https://git.twonly.eu/twonly/twonly-app-dependencies.git [submodule "lib/src/localization/translations"] path = lib/src/localization/translations # url = ssh://git@git.twonly.eu:22222/twonly/twonly-translations.git From 5a56a092a04dc16bbc697833345fe0d6b8d7fcf9 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 27 Jan 2026 11:53:42 +0100 Subject: [PATCH 11/22] adding roadmap --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e66c36d..6ec5754 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,24 @@ If you decide to give twonly a try, please keep in mind that it is still in its - Privacy friendly - Everything is stored on the device - The backend is hosted exclusively in Europe -## Planned +## Roadmap -- For Android: Optional support for [UnifiedPush](https://unifiedpush.org/) -- For Android: Reproducible Builds -- Implementing [Sealed Sender](https://signal.org/blog/sealed-sender/) to minimize metadata -- Switch from the Signal-Protocol to [MLS](https://github.com/openmls/openmls) for Post-Quantum-Crypto support -- And, of course, many more features such as dog filters, E2EE cloud backup, and more. +### Currently + +- Focus on user-friendliness so that people enjoy using the app + - User discovery without a phone number + - Passwordless recovery without a phone number +- Implementation of features so that Snapchat can actually be replaced + - E2EE cloud backup of memories + - Importing memories from Snapchat + +### Next on the bucket list + +- For Android: Support for [UnifiedPush] (https://unifiedpush.org/) +- For Android: Reproducible builds +- Implementation of [Sealed Sender](https://signal.org/blog/sealed-sender/) (or a similar protocol) to minimize metadata +- Switch from the Signal protocol to [MLS](https://github.com/openmls/openmls) for post-quantum crypto support +- Decentralize the server so that anyone can run their own server ## Security Issues @@ -46,9 +57,9 @@ If you discover a security issue in twonly, please adhere to the coordinated vul us your report to security@twonly.eu. We also offer for critical security issues a small bug bounties, but we can not guarantee a bounty currently :/ -## Contribution + ## Development From e8b9466e15c139864ea3545197c73ab0a84a8a9c Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 22:58:15 +0100 Subject: [PATCH 12/22] remove gender from survey --- .../user_study/user_study_questionnaire.view.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/src/views/user_study/user_study_questionnaire.view.dart b/lib/src/views/user_study/user_study_questionnaire.view.dart index 8713323..09469be 100644 --- a/lib/src/views/user_study/user_study_questionnaire.view.dart +++ b/lib/src/views/user_study/user_study_questionnaire.view.dart @@ -16,8 +16,6 @@ class UserStudyQuestionnaire extends StatefulWidget { class _UserStudyQuestionnaireState extends State { final Map _responses = { - 'gender': null, - 'gender_free': '', 'age': null, 'education': null, 'education_free': '', @@ -77,15 +75,6 @@ class _UserStudyQuestionnaireState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _sectionTitle('Demografische Daten'), - _questionText('Was ist dein Geschlecht?'), - _buildRadioList( - ['Männlich', 'Weiblich', 'Divers', 'Keine Angabe'], - 'gender', - ), - _buildTextField( - 'Freitext (optional)', - (val) => _responses['gender_free'] = val, - ), _questionText('Wie alt bist du?'), _buildRadioList( [ From 90bf634f59ef684fdf8b43beaae68cb175a62714 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:01:29 +0100 Subject: [PATCH 13/22] bug: verifies user still has an account before trying to download a media file --- .../api/mediafiles/download.service.dart | 44 ++++++++++++++++--- lib/src/services/api/messages.dart | 11 +++-- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index 42a4195..8ebf018 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -23,10 +23,44 @@ Future tryDownloadAllMediaFiles({bool force = false}) async { await twonlyDB.mediaFilesDao.getAllMediaFilesPendingDownload(); for (final mediaFile in mediaFiles) { - await startDownloadMedia(mediaFile, force); + if (await canMediaFileBeDownloaded(mediaFile)) { + await startDownloadMedia(mediaFile, force); + } } } +Future canMediaFileBeDownloaded(MediaFile mediaFile) async { + final messages = + await twonlyDB.messagesDao.getMessagesByMediaId(mediaFile.mediaId); + + // Verify that the sender of the original image / message does still exists. + // If not delete the message as it can not be downloaded from the server anymore. + + if (messages.length != 1) { + Log.error('A media for download must have one original message.'); + return false; + } + + if (messages.first.senderId == null) { + Log.error('A media for download must have a sender id.'); + return false; + } + + final contact = + await twonlyDB.contactsDao.getContactById(messages.first.senderId!); + + if (contact == null || contact.accountDeleted) { + Log.info( + 'Sender does not exists anymore. Delete media file and message.', + ); + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + await twonlyDB.messagesDao.deleteMessagesById(messages.first.messageId); + return false; + } + + return true; +} + enum DownloadMediaTypes { video, image, @@ -90,11 +124,9 @@ Future handleDownloadStatusUpdate(TaskStatusUpdate update) async { failed = false; } else { failed = true; - if (update.responseStatusCode != null) { - Log.error( - 'Got invalid response status code: ${update.responseStatusCode}', - ); - } + Log.error( + 'Got invalid response status code: ${update.responseStatusCode}', + ); } } else { Log.info('Got ${update.status} for $mediaId'); diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index 6db4af4..ea3bafc 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -74,6 +74,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ } receiptId = receipt.receiptId; + final contact = + await twonlyDB.contactsDao.getContactById(receipt.contactId); + if (contact == null || contact.accountDeleted) { + Log.warn('Will not send message again as user does not exist anymore.'); + await twonlyDB.receiptsDao.deleteReceipt(receiptId); + return null; + } + if (!onlyReturnEncryptedData && receipt.ackByServerAt != null && receipt.markForRetry == null) { @@ -177,9 +185,6 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ if (receiptId != null) { await twonlyDB.receiptsDao.deleteReceipt(receiptId); } - if (receipt != null) { - await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); - } } return null; } From 4f6bffa61ac838abb3deee68ac20e476a8248f61 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:02:36 +0100 Subject: [PATCH 14/22] fix: images was not reuploaded in case of a reupload request --- lib/main.dart | 9 +++++++++ lib/src/database/daos/mediafiles.dao.dart | 14 ++++++++++++++ lib/src/services/api/client2client/media.c2c.dart | 4 +++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 2b06327..0dc258d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -44,6 +44,15 @@ void main() async { } unawaited(performTwonlySafeBackup()); + + if (gUser.appVersion < 90) { + // BUG: Requested media files for reupload where not reuploaded because the wrong state... + await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); + await updateUserdata((u) { + u.appVersion = 90; + return u; + }); + } } globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index dff6008..414cc29 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -131,4 +131,18 @@ class MediaFilesDao extends DatabaseAccessor ..limit(100)) .watch(); } + + Future updateAllRetransmissionUploadingState() async { + await (update(mediaFiles) + ..where( + (t) => + t.uploadState.equals(UploadState.uploading.name) & + t.reuploadRequestedBy.isNotNull(), + )) + .write( + const MediaFilesCompanion( + uploadState: Value(UploadState.preprocessing), + ), + ); + } } diff --git a/lib/src/services/api/client2client/media.c2c.dart b/lib/src/services/api/client2client/media.c2c.dart index d0c8e3a..6795acd 100644 --- a/lib/src/services/api/client2client/media.c2c.dart +++ b/lib/src/services/api/client2client/media.c2c.dart @@ -6,6 +6,7 @@ import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; @@ -178,9 +179,10 @@ Future handleMediaUpdate( await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, MediaFilesCompanion( - uploadState: const Value(UploadState.uploading), + uploadState: const Value(UploadState.preprocessing), reuploadRequestedBy: Value(reuploadRequestedBy), ), ); + unawaited(startBackgroundMediaUpload(MediaFileService(mediaFile))); } } From 3aea367dfd4d189e6a743432e25ced869f5f16d5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:02:43 +0100 Subject: [PATCH 15/22] remove unused code --- lib/src/database/daos/messages.dao.dart | 139 ------------------------ 1 file changed, 139 deletions(-) diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 998e51f..72a8d59 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -131,53 +131,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { } } - // Future> getAllMessagesPendingDownloading() { - // return (select(messages) - // ..where( - // (t) => - // t.downloadState.equals(DownloadState.downloaded.index).not() & - // t.messageOtherId.isNotNull() & - // t.errorWhileSending.equals(false) & - // t.kind.equals(MessageKind.media.name), - // )) - // .get(); - // } - - // Future> getAllNonACKMessagesFromUser() { - // return (select(messages) - // ..where( - // (t) => - // t.acknowledgeByUser.equals(false) & - // t.messageOtherId.isNull() & - // t.errorWhileSending.equals(false) & - // t.sendAt.isBiggerThanValue( - // clock.now().subtract(const Duration(minutes: 10)), - // ), - // )) - // .get(); - // } - - // Stream> getAllStoredMediaFiles() { - // return (select(messages) - // ..where((t) => t.mediaStored.equals(true)) - // ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) - // .watch(); - // } - - // Future> getAllMessagesPendingUpload() { - // return (select(messages) - // ..where( - // (t) => - // t.acknowledgeByServer.equals(false) & - // t.messageOtherId.isNull() & - // t.mediaUploadId.isNotNull() & - // t.downloadState.equals(DownloadState.pending.index) & - // t.errorWhileSending.equals(false) & - // t.kind.equals(MessageKind.media.name), - // )) - // .get(); - // } - Future openedAllTextMessages(String groupId) { final updates = MessagesCompanion(openedAt: Value(clock.now())); return (update(messages) @@ -322,32 +275,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return members.length == actions.length; } - // Future updateMessageByOtherUser( - // int userId, - // int messageId, - // MessagesCompanion updatedValues, - // ) { - // return (update(messages) - // ..where( - // (c) => c.contactId.equals(userId) & c.messageId.equals(messageId), - // )) - // .write(updatedValues); - // } - - // Future updateMessageByOtherMessageId( - // int userId, - // int messageOtherId, - // MessagesCompanion updatedValues, - // ) { - // return (update(messages) - // ..where( - // (c) => - // c.contactId.equals(userId) & - // c.messageOtherId.equals(messageOtherId), - // )) - // .write(updatedValues); - // } - Future updateMessageId( String messageId, MessagesCompanion updatedValues, @@ -445,27 +372,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { }); } - // Future deleteMessagesByContactId(int contactId) { - // return (delete(messages) - // ..where( - // (t) => t.contactId.equals(contactId) & t.mediaStored.equals(false), - // )) - // .go(); - // } - - // Future deleteMessagesByContactIdAndOtherMessageId( - // int contactId, - // int messageOtherId, - // ) { - // return (delete(messages) - // ..where( - // (t) => - // t.contactId.equals(contactId) & - // t.messageOtherId.equals(messageOtherId), - // )) - // .go(); - // } - Future deleteMessagesById(String messageId) { return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); } @@ -474,24 +380,6 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { return (delete(messages)..where((t) => t.groupId.equals(groupId))).go(); } - // Future deleteAllMessagesByContactId(int contactId) { - // return (delete(messages)..where((t) => t.contactId.equals(contactId))).go(); - // } - - // Future containsOtherMessageId( - // int fromUserId, - // int messageOtherId, - // ) async { - // final query = select(messages) - // ..where( - // (t) => - // t.messageOtherId.equals(messageOtherId) & - // t.contactId.equals(fromUserId), - // ); - // final entry = await query.get(); - // return entry.isNotEmpty; - // } - SingleOrNullSelectable getMessageById(String messageId) { return select(messages)..where((t) => t.messageId.equals(messageId)); } @@ -519,31 +407,4 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { ..orderBy([(t) => OrderingTerm.desc(t.createdAt)])) .watch(); } - - // Future> getMessagesByMediaUploadId(int mediaUploadId) async { - // return (select(messages) - // ..where((t) => t.mediaUploadId.equals(mediaUploadId))) - // .get(); - // } - - // SingleOrNullSelectable getMessageByOtherMessageId( - // int fromUserId, - // int messageId, - // ) { - // return select(messages) - // ..where( - // (t) => - // t.messageOtherId.equals(messageId) & t.contactId.equals(fromUserId), - // ); - // } - - // SingleOrNullSelectable getMessageByIdAndContactId( - // int fromUserId, - // int messageId, - // ) { - // return select(messages) - // ..where( - // (t) => t.messageId.equals(messageId) & t.contactId.equals(fromUserId), - // ); - // } } From a6b673afadc10fe7f4243230291a8572ee072b7f Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:03:35 +0100 Subject: [PATCH 16/22] fix: missing translation --- lib/src/localization/translations | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 9d04e9e..4caaa3d 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 9d04e9e1d0cdba8f1be4b0cbba341706c3cffac9 +Subproject commit 4caaa3d91aaf1ac2f13160ba770a2880c26bd229 From 47904275e1eb6a91089d72086d50534d95c3f103 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:24:25 +0100 Subject: [PATCH 17/22] fix: authentication not enforced --- dependencies | 2 +- lib/main.dart | 20 ++++++++++--------- .../generated/app_localizations.dart | 2 +- .../generated/app_localizations_en.dart | 2 +- .../generated/app_localizations_sv.dart | 2 +- lib/src/utils/misc.dart | 8 ++++++-- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/dependencies b/dependencies index 7930d97..3a3a7e5 160000 --- a/dependencies +++ b/dependencies @@ -1 +1 @@ -Subproject commit 7930d9727019344238297d810661bc3e8f724c37 +Subproject commit 3a3a7e5a6323da5413e3dd8c21abfa7cbe1c3a6f diff --git a/lib/main.dart b/lib/main.dart index 0dc258d..ba1eaa8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -44,15 +44,6 @@ void main() async { } unawaited(performTwonlySafeBackup()); - - if (gUser.appVersion < 90) { - // BUG: Requested media files for reupload where not reuploaded because the wrong state... - await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); - await updateUserdata((u) { - u.appVersion = 90; - return u; - }); - } } globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path; @@ -73,6 +64,17 @@ void main() async { apiService = ApiService(); twonlyDB = TwonlyDB(); + if (user != null) { + if (gUser.appVersion < 90) { + // BUG: Requested media files for reupload where not reuploaded because the wrong state... + await twonlyDB.mediaFilesDao.updateAllRetransmissionUploadingState(); + await updateUserdata((u) { + u.appVersion = 90; + return u; + }); + } + } + await twonlyDB.messagesDao.purgeMessageTable(); await twonlyDB.receiptsDao.purgeReceivedReceipts(); unawaited(MediaFileService.purgeTempFolder()); diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index fb8ea1b..7b6c04b 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -667,7 +667,7 @@ abstract class AppLocalizations { /// No description provided for @settingsAccount. /// /// In en, this message translates to: - /// **'Konto'** + /// **'Account'** String get settingsAccount; /// No description provided for @settingsSubscription. diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index fecc2ac..e4d51a4 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -313,7 +313,7 @@ class AppLocalizationsEn extends AppLocalizations { String get settingsProfileEditDisplayNameNew => 'New Displayname'; @override - String get settingsAccount => 'Konto'; + String get settingsAccount => 'Account'; @override String get settingsSubscription => 'Subscription'; diff --git a/lib/src/localization/generated/app_localizations_sv.dart b/lib/src/localization/generated/app_localizations_sv.dart index bcd7c52..05a47b1 100644 --- a/lib/src/localization/generated/app_localizations_sv.dart +++ b/lib/src/localization/generated/app_localizations_sv.dart @@ -313,7 +313,7 @@ class AppLocalizationsSv extends AppLocalizations { String get settingsProfileEditDisplayNameNew => 'New Displayname'; @override - String get settingsAccount => 'Konto'; + String get settingsAccount => 'Account'; @override String get settingsSubscription => 'Subscription'; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 031bad0..2db2272 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -159,8 +159,12 @@ Future authenticateUser( } } on LocalAuthException catch (e) { Log.error(e.toString()); - if (!force) { - return true; + if (e.code == LocalAuthExceptionCode.noBiometricHardware || + e.code == LocalAuthExceptionCode.noBiometricsEnrolled || + e.code == LocalAuthExceptionCode.noCredentialsSet) { + if (!force) { + return true; + } } } return false; From 17c22c2f80b94a64f60b94f30bce28a9ba9fce80 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:52:11 +0100 Subject: [PATCH 18/22] fix zoom issue on ios --- .../camera/camera_preview_components/zoom_selector.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/src/views/camera/camera_preview_components/zoom_selector.dart b/lib/src/views/camera/camera_preview_components/zoom_selector.dart index e3f7032..6397b29 100644 --- a/lib/src/views/camera/camera_preview_components/zoom_selector.dart +++ b/lib/src/views/camera/camera_preview_components/zoom_selector.dart @@ -152,7 +152,8 @@ class _CameraZoomButtonsState extends State { ), onPressed: () async { if (showWideAngleZoomIOS && - widget.selectedCameraDetails.cameraId == 2) { + widget.selectedCameraDetails.cameraId == + _wideCameraIndex) { await widget.selectCamera(0, true); } else { widget.updateScaleFactor(1.0); @@ -175,6 +176,12 @@ class _CameraZoomButtonsState extends State { final level = min(await widget.controller.getMaxZoomLevel(), 2) .toDouble(); + + if (showWideAngleZoomIOS && + widget.selectedCameraDetails.cameraId == + _wideCameraIndex) { + await widget.selectCamera(0, true); + } widget.updateScaleFactor(level); }, child: Text( From 2b1952a7ada3846b2578ca40579913e603e8c38a Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 7 Feb 2026 23:52:35 +0100 Subject: [PATCH 19/22] fix: allow multiple instances to open the database --- lib/src/database/twonly.db.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index 1d27de6..a03602a 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -75,6 +75,7 @@ class TwonlyDB extends _$TwonlyDB { name: 'twonly', native: const DriftNativeOptions( databaseDirectory: getApplicationSupportDirectory, + shareAcrossIsolates: true, ), ); } From 21fbd8a04cdff6a7d13157407702850c4875f49d Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 8 Feb 2026 00:08:04 +0100 Subject: [PATCH 20/22] fix: filter should be none when app is starting --- .../camera_preview_components/main_camera_controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 dd5364f..da13414 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 @@ -83,7 +83,7 @@ class MainCameraController { CustomPaint? facePaint; Offset? focusPointOffset; - FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip; + FaceFilterType _currentFilterType = FaceFilterType.none; FaceFilterType get currentFilterType => _currentFilterType; Future closeCamera() async { From 1b819c5b08943594d627b713bfb09e97842b1a10 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 8 Feb 2026 00:08:30 +0100 Subject: [PATCH 21/22] fix: add link handler to app links listener --- lib/src/services/intent/links.intent.dart | 26 +++++++++++++---------- lib/src/views/home.view.dart | 9 +++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/src/services/intent/links.intent.dart b/lib/src/services/intent/links.intent.dart index cf4e7fa..425d60a 100644 --- a/lib/src/services/intent/links.intent.dart +++ b/lib/src/services/intent/links.intent.dart @@ -19,17 +19,17 @@ import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/contact/contact.view.dart'; import 'package:twonly/src/views/public_profile.view.dart'; -Future handleIntentUrl(BuildContext context, Uri uri) async { - if (!uri.scheme.startsWith('http')) return; - if (uri.host != 'me.twonly.eu') return; - if (uri.hasEmptyPath) return; +Future handleIntentUrl(BuildContext context, Uri uri) async { + if (!uri.scheme.startsWith('http')) return false; + if (uri.host != 'me.twonly.eu') return false; + if (uri.hasEmptyPath) return false; final publicKey = uri.hasFragment ? uri.fragment : null; final userPaths = uri.path.split('/'); - if (userPaths.length != 2) return; + if (userPaths.length != 2) return false; final username = userPaths[1]; - if (!context.mounted) return; + if (!context.mounted) return false; if (username == gUser.username) { await Navigator.push( @@ -40,7 +40,7 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { }, ), ); - return; + return true; } Log.info( @@ -48,7 +48,7 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { ); final contacts = await twonlyDB.contactsDao.getContactsByUsername(username); if (contacts.isEmpty) { - if (!context.mounted) return; + if (!context.mounted) return true; Uint8List? publicKeyBytes; if (publicKey != null) { publicKeyBytes = base64Url.decode(publicKey); @@ -72,7 +72,7 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { if (storedPublicKey == null || receivedPublicKey.isEmpty || !context.mounted) { - return; + return true; } if (storedPublicKey.equals(receivedPublicKey)) { if (!contact.verified) { @@ -112,6 +112,7 @@ Future handleIntentUrl(BuildContext context, Uri uri) async { Log.warn(e); } } + return true; } Future handleIntentMediaFile( @@ -160,12 +161,15 @@ Future handleIntentSharedFile( ); continue; } - Log.info('got file via intent ${file.type} ${file.value}'); + + Log.info('got file via intent ${file.type}'); switch (file.type) { case SharedMediaType.URL: if (file.value?.startsWith('http') ?? false) { - onUrlCallBack(Uri.parse(file.value!)); + final uri = Uri.parse(file.value!); + Log.info('Got link via handle intent share file: ${uri.scheme}'); + onUrlCallBack(uri); } case SharedMediaType.IMAGE: var type = MediaType.image; diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index f67c335..b4e5069 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -116,7 +116,14 @@ class HomeViewState extends State { // Subscribe to all events (initial link and further) _deepLinkSub = AppLinks().uriLinkStream.listen((uri) async { - if (mounted) await handleIntentUrl(context, uri); + if (mounted) { + Log.info('Got link via app links: ${uri.scheme}'); + if (!await handleIntentUrl(context, uri)) { + if (uri.scheme.startsWith('http')) { + _mainCameraController.setSharedLinkForPreview(uri); + } + } + } }); _intentStreamSub = FlutterSharingIntent.instance.getMediaStream().listen( From 90bf944bb326b570ad91bb9a2912e5f149357839 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 8 Feb 2026 00:17:02 +0100 Subject: [PATCH 22/22] update version --- CHANGELOG.md | 9 +++++++++ pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5309a..92b3f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.0.90 + +- Fixes issue that media files where not reuploaded +- Fixes iOS zooming issue when switching between .5 and x1 +- Fixes biometric auth bypass when opening a twonly/reopen send image +- Fixes that media files could not be downloaded in case the contact deleted his account +- Fixes database issue in case twonly is opened multiple times +- Fixes typos in translation + ## 0.0.87 - Adds link preview to images diff --git a/pubspec.lock b/pubspec.lock index 6b30fbb..1227333 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -905,7 +905,7 @@ packages: path: "dependencies/hashlib" relative: true source: path - version: "2.2.0" + version: "2.3.0" hashlib_codecs: dependency: "direct overridden" description: @@ -1259,7 +1259,7 @@ packages: path: "dependencies/no_screenshot" relative: true source: path - version: "0.3.2-beta.3" + version: "0.3.2" objective_c: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5ac7cf8..5cf436c 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.89+89 +version: 0.0.90+90 environment: sdk: ^3.6.0