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 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/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 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/assets/filters/beard_upper_lip_green.webp b/assets/filters/beard_upper_lip_green.webp new file mode 100644 index 0000000..4046f65 Binary files /dev/null and b/assets/filters/beard_upper_lip_green.webp differ diff --git a/assets/filters/hat_black.webp b/assets/filters/hat_black.webp new file mode 100644 index 0000000..fbc44c0 Binary files /dev/null and b/assets/filters/hat_black.webp differ 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 2140a8d..ba1eaa8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -64,7 +64,19 @@ 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()); await initFileDownloader(); 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/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), - // ); - // } } 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..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, ), ); } @@ -166,6 +167,7 @@ class TwonlyDB extends _$TwonlyDB { )) .go(); await delete(receipts).go(); + await delete(receivedReceipts).go(); await update(contacts).write( const ContactsCompanion( avatarSvgCompressed: Value(null), 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/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 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))); } } 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; } 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/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; 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 cfbef1a..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 @@ -65,6 +65,11 @@ class MainCameraController { setState(); } + void onImageSend() { + scannedUrl = ''; + setState(); + } + final BarcodeScanner _barcodeScanner = BarcodeScanner(); final FaceDetector _faceDetector = FaceDetector( options: FaceDetectorOptions( @@ -78,7 +83,7 @@ class MainCameraController { CustomPaint? facePaint; Offset? focusPointOffset; - FaceFilterType _currentFilterType = FaceFilterType.beardUpperLip; + FaceFilterType _currentFilterType = FaceFilterType.none; FaceFilterType get currentFilterType => _currentFilterType; Future closeCamera() async { @@ -392,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, ), ), 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( 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/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; } } 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..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 @@ -48,7 +48,10 @@ 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 +60,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, diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 355d38e..b4e5069 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; }); } @@ -115,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( 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; 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..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': '', @@ -51,7 +49,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; }); @@ -75,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( [ 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 7427a5d..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.87+87 +version: 0.0.90+90 environment: sdk: ^3.6.0