diff --git a/lib/app.dart b/lib/app.dart index c4f9c77..bc7d9cd 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -3,6 +3,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; +import 'package:twonly/src/services/api/media_send.dart'; import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/onboarding.view.dart'; @@ -71,7 +72,9 @@ class _AppState extends State with WidgetsBindingObserver { Future initAsync() async { setUserPlan(); - apiService.connect(); + await apiService.connect(); + // call this function so invalid media files are get purged + retryMediaUpload(true); } @override diff --git a/lib/main.dart b/lib/main.dart index 2064686..7717a24 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,6 +42,7 @@ void main() async { apiService = ApiService(); twonlyDB = TwonlyDatabase(); await twonlyDB.messagesDao.resetPendingDownloadState(); + await twonlyDB.messagesDao.handleMediaFilesOlderThan7Days(); // purge media files in the background purgeReceivedMediaFiles(); diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index 3edd315..11728e7 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -69,6 +69,22 @@ class MessagesDao extends DatabaseAccessor .write(MessagesCompanion(contentJson: Value(null))); } + Future handleMediaFilesOlderThan7Days() { + /// media files will be deleted by the server after 7 days, so delete them here also + return (update(messages) + ..where( + (t) => (t.kind.equals(MessageKind.media.name) & + t.openedAt.isNull() & + t.messageOtherId.isNull() & + (t.sendAt.isSmallerThanValue( + DateTime.now().subtract( + Duration(days: 8), + ), + ))), + )) + .write(MessagesCompanion(errorWhileSending: Value(true))); + } + Future> getAllMessagesPendingDownloading() { return (select(messages) ..where( diff --git a/lib/src/database/tables/messages_table.dart b/lib/src/database/tables/messages_table.dart index 7094a1e..eb3584f 100644 --- a/lib/src/database/tables/messages_table.dart +++ b/lib/src/database/tables/messages_table.dart @@ -13,7 +13,8 @@ enum MessageKind { flameSync, opened, ack, - pushKey + pushKey, + receiveMediaError, } enum DownloadState { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index c1352ee..8407526 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -228,7 +228,7 @@ "checkoutOptions": "Optionen", "checkoutPayYearly": "Jährlich bezahlen", "checkoutTotal": "Gesamt", - "selectPaymentMethode": "Zahlungsmethode auswählen", + "selectPaymentMethod": "Zahlungsmethode auswählen", "twonlyCredit": "twonly-Guthaben", "notEnoughCredit": "Du hast nicht genügend Guthaben!", "chargeCredit": "Guthaben aufladen", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 88167ac..befc3cf 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -386,7 +386,7 @@ "refund": "Refund", "checkoutPayYearly": "Pay yearly", "checkoutTotal": "Total", - "selectPaymentMethode": "Select Payment Method", + "selectPaymentMethod": "Select Payment Method", "twonlyCredit": "twonly-Credit", "notEnoughCredit": "You do not have enough credit!", "chargeCredit": "Charge credit", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index f6ec53e..ed9327d 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -1382,11 +1382,11 @@ abstract class AppLocalizations { /// **'Total'** String get checkoutTotal; - /// No description provided for @selectPaymentMethode. + /// No description provided for @selectPaymentMethod. /// /// In en, this message translates to: /// **'Select Payment Method'** - String get selectPaymentMethode; + String get selectPaymentMethod; /// No description provided for @twonlyCredit. /// diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index d55e9b4..d980bcc 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -725,7 +725,7 @@ class AppLocalizationsDe extends AppLocalizations { String get checkoutTotal => 'Gesamt'; @override - String get selectPaymentMethode => 'Zahlungsmethode auswählen'; + String get selectPaymentMethod => 'Zahlungsmethode auswählen'; @override String get twonlyCredit => 'twonly-Guthaben'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 46fa612..14d8841 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -720,7 +720,7 @@ class AppLocalizationsEn extends AppLocalizations { String get checkoutTotal => 'Total'; @override - String get selectPaymentMethode => 'Select Payment Method'; + String get selectPaymentMethod => 'Select Payment Method'; @override String get twonlyCredit => 'twonly-Credit'; diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index 1f866af..cbd8181 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -39,11 +39,12 @@ class MessageJson { final int? messageId; DateTime timestamp; - MessageJson( - {required this.kind, - this.messageId, - required this.content, - required this.timestamp}); + MessageJson({ + required this.kind, + this.messageId, + required this.content, + required this.timestamp, + }); @override String toString() { diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 9b814f0..648c853 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -51,7 +51,7 @@ class ApiService { // reconnection params Timer? reconnectionTimer; - // int _reconnectionDelay = 5; + int _reconnectionDelay = 5; final HashMap messagesV0 = HashMap(); IOWebSocketChannel? _channel; @@ -81,7 +81,7 @@ class ApiService { if (!globalIsAppInBackground) { retransmitRawBytes(); tryTransmitMessages(); - retryMediaUpload(); + retryMediaUpload(false); tryDownloadAllMediaFiles(); notifyContactsAboutProfileChange(); twonlyDB.markUpdated(); @@ -92,6 +92,7 @@ class ApiService { Future onConnected() async { await authenticate(); + _reconnectionDelay = 5; globalCallbackConnectionState(true); } @@ -100,6 +101,12 @@ class ApiService { isAuthenticated = false; globalCallbackConnectionState(false); await twonlyDB.messagesDao.resetPendingDownloadState(); + reconnectionTimer ??= Timer(Duration(seconds: _reconnectionDelay), () { + Log.info("starting with reconnection."); + reconnectionTimer = null; + connect(); + }); + _reconnectionDelay += 5; } Future close(Function callback) async { @@ -113,11 +120,13 @@ class ApiService { callback(); } - Future connect() async { + Future connect({bool force = false}) async { + if (reconnectionTimer != null && !force) { + return false; + } + reconnectionTimer?.cancel(); final user = await getUser(); if (user != null && user.isDemoUser) { - print("DEMO user"); - // the demo user should not be able to connect to the API server... globalCallbackConnectionState(true); return false; } @@ -126,9 +135,7 @@ class ApiService { return true; } // ensure that the connect function is not called again by the timer. - if (reconnectionTimer != null) { - reconnectionTimer!.cancel(); - } + reconnectionTimer?.cancel(); isAuthenticated = false; diff --git a/lib/src/services/api/media_received.dart b/lib/src/services/api/media_received.dart index 2a0de3a..8730039 100644 --- a/lib/src/services/api/media_received.dart +++ b/lib/src/services/api/media_received.dart @@ -12,8 +12,7 @@ import 'package:http/http.dart' as http; // import 'package:twonly/src/providers/api/api_utils.dart'; import 'package:twonly/src/services/api/media_send.dart'; import 'package:cryptography_plus/cryptography_plus.dart'; -import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart' - as client; +import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -75,7 +74,8 @@ Future isAllowedToDownload(bool isVideo) async { return false; } -Future startDownloadMedia(Message message, bool force) async { +Future startDownloadMedia(Message message, bool force, + {int retryCounter = 0}) async { if (message.contentJson == null) return; if (downloadStartedForMediaReceived[message.messageId] != null) { DateTime started = downloadStartedForMediaReceived[message.messageId]!; @@ -151,12 +151,10 @@ Future startDownloadMedia(Message message, bool force) async { }, onDone: () async { if (r.statusCode != 200) { Log.error("Download error: $r"); - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - MessagesCompanion( - errorWhileSending: Value(true), - ), - ); + if (r.statusCode == 418) { + Log.error("Got custom error code: ${chunks.toList()}"); + handleMediaError(message); + } return; } @@ -172,13 +170,18 @@ Future startDownloadMedia(Message message, bool force) async { offset += chunk.length; } await writeMediaFile(message.messageId, "encrypted", bytes); - handleEncryptedFile(message, encryptedBytesTmp: bytes); + handleEncryptedFile(message, + encryptedBytesTmp: bytes, retryCounter: retryCounter); return; }); }); } -Future handleEncryptedFile(Message msg, {Uint8List? encryptedBytesTmp}) async { +Future handleEncryptedFile( + Message msg, { + Uint8List? encryptedBytesTmp, + int retryCounter = 0, +}) async { Uint8List? encryptedBytes = encryptedBytesTmp ?? await readMediaFile(msg.messageId, "encrypted"); @@ -211,16 +214,16 @@ Future handleEncryptedFile(Message msg, {Uint8List? encryptedBytesTmp}) async { await writeMediaFile(msg.messageId, "png", imageBytes); } catch (e) { - Log.error("Decryption error: $e"); - await twonlyDB.messagesDao.updateMessageByMessageId( - msg.messageId, - MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - // answers with ok, so the server will delete the message - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; + if (retryCounter >= 1) { + Log.error( + "could not decrypt the media file in the second try. reporting error to user: $e"); + handleMediaError(msg); + return; + } + Log.error("could not decrypt the media file trying again: $e"); + startDownloadMedia(msg, true, retryCounter: retryCounter + 1); + // try downloading again.... + return; } await twonlyDB.messagesDao.updateMessageByMessageId( diff --git a/lib/src/services/api/media_send.dart b/lib/src/services/api/media_send.dart index e5abd52..4ff5527 100644 --- a/lib/src/services/api/media_send.dart +++ b/lib/src/services/api/media_send.dart @@ -103,7 +103,7 @@ Future checkForFailedUploads() async { } final lockingHandleMediaFile = Mutex(); -Future retryMediaUpload({int maxRetries = 3}) async { +Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { if (maxRetries == 0) { Log.error("retried media upload 3 times. abort retrying"); return; @@ -116,16 +116,23 @@ Future retryMediaUpload({int maxRetries = 3}) async { Log.info("re uploading ${mediaFiles.length} media files."); for (final mediaFile in mediaFiles) { if (mediaFile.messageIds == null || mediaFile.metadata == null) { - // the media upload was canceled, - if (mediaFile.uploadTokens != null) { - /// the file was already uploaded. - /// notify the server to remove the upload - apiService.getDownloadTokens(mediaFile.uploadTokens!.uploadToken, 0); + if (appRestarted) { + /// When the app got restarted and the messageIds or the metadata is not + /// set then the app was closed before the images was send. + + // the media upload was canceled, + if (mediaFile.uploadTokens != null) { + /// the file was already uploaded. + /// notify the server to remove the upload + apiService.getDownloadTokens( + mediaFile.uploadTokens!.uploadToken, 0); + } + await twonlyDB.mediaUploadsDao + .deleteMediaUpload(mediaFile.mediaUploadId); + Log.info( + "upload can be removed, the finalized function was never called...", + ); } - await twonlyDB.mediaUploadsDao - .deleteMediaUpload(mediaFile.mediaUploadId); - Log.info( - "upload can be removed, the finalized function was never called..."); continue; } @@ -138,7 +145,7 @@ Future retryMediaUpload({int maxRetries = 3}) async { return false; }); if (retry) { - await retryMediaUpload(maxRetries: maxRetries - 1); + await retryMediaUpload(false, maxRetries: maxRetries - 1); } } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index c40e6c1..e089a4b 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -112,6 +112,7 @@ Future> getAllMessagesForRetransmitting() async { Future sendRetransmitMessage( String stateId, RetransmitMessage msg) async { + Log.info("Sending ${msg.messageId}"); Result resp = await apiService.sendTextMessage(msg.userId, msg.bytes, msg.pushData); @@ -130,8 +131,8 @@ Future sendRetransmitMessage( } if (resp.isSuccess) { + retry = false; if (msg.messageId != null) { - retry = false; await twonlyDB.messagesDao.updateMessageByMessageId( msg.messageId!, MessagesCompanion(acknowledgeByServer: Value(true)), diff --git a/lib/src/services/api/server_messages.dart b/lib/src/services/api/server_messages.dart index bbc994e..3fe8f5c 100644 --- a/lib/src/services/api/server_messages.dart +++ b/lib/src/services/api/server_messages.dart @@ -86,23 +86,35 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { } } - case MessageKind.opened: - final update = MessagesCompanion(openedAt: Value(message.timestamp)); - await twonlyDB.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageId!, - update, - ); - final openedMessage = await twonlyDB.messagesDao - .getMessageByMessageId(message.messageId!) - .getSingleOrNull(); - if (openedMessage != null && - openedMessage.kind == MessageKind.textMessage) { - await twonlyDB.messagesDao.openedAllNonMediaMessagesFromOtherUser( + case MessageKind.receiveMediaError: + if (message.messageId != null) { + await twonlyDB.messagesDao.updateMessageByOtherUser( fromUserId, + message.messageId!, + MessagesCompanion( + errorWhileSending: Value(true), + ), ); } + case MessageKind.opened: + if (message.messageId != null) { + final update = MessagesCompanion(openedAt: Value(message.timestamp)); + await twonlyDB.messagesDao.updateMessageByOtherUser( + fromUserId, + message.messageId!, + update, + ); + final openedMessage = await twonlyDB.messagesDao + .getMessageByMessageId(message.messageId!) + .getSingleOrNull(); + if (openedMessage != null && + openedMessage.kind == MessageKind.textMessage) { + await twonlyDB.messagesDao.openedAllNonMediaMessagesFromOtherUser( + fromUserId, + ); + } + } break; case MessageKind.rejectRequest: diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index 3b55360..a88111b 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -1,6 +1,8 @@ +import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; +import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart' as client; @@ -66,3 +68,23 @@ Future rejectUser(int contactId) async { ), ); } + +Future handleMediaError(Message message) async { + await twonlyDB.messagesDao.updateMessageByMessageId( + message.messageId, + MessagesCompanion( + errorWhileSending: Value(true), + ), + ); + if (message.messageOtherId != null) { + encryptAndSendMessageAsync( + null, + message.contactId, + MessageJson( + kind: MessageKind.receiveMediaError, + timestamp: DateTime.now(), + content: MessageContent(), + messageId: message.messageOtherId), + ); + } +} diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 862822f..ad22f17 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -112,7 +112,7 @@ String formatDuration(int seconds) { } } -InputDecoration getInputDecoration(context, hintText) { +InputDecoration getInputDecoration(BuildContext context, String hintText) { final primaryColor = Theme.of(context).colorScheme.primary; // Get the primary color return InputDecoration( @@ -279,7 +279,7 @@ Future insertDemoContacts() async { if (config['accepted'] ?? false) { for (var i = 0; i < 20; i++) { int chatId = Random().nextInt(chatMessages.length); - int? messageId = await twonlyDB.messagesDao.insertMessage( + await twonlyDB.messagesDao.insertMessage( MessagesCompanion( contactId: Value(userId), kind: Value(MessageKind.textMessage), diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 964d2c6..0993edd 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -1,7 +1,10 @@ import 'dart:convert'; +import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; import 'package:twonly/src/model/json/userdata.dart'; +import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/utils/log.dart'; Future isUserCreated() async { @@ -28,6 +31,17 @@ Future getUser() async { } } +Future updateUsersPlan(BuildContext context, String planId) async { + context.read().plan = planId; + var user = await getUser(); + if (user != null) { + user.subscriptionPlan = planId; + await updateUser(user); + } + if (!context.mounted) return; + context.read().updatePlan(planId); +} + Future updateUser(UserData userData) async { final storage = FlutterSecureStorage(); storage.write(key: "userData", value: jsonEncode(userData)); diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index ac92bb2..253b2f6 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -173,7 +173,7 @@ class _ChatListViewState extends State { : RefreshIndicator( onRefresh: () async { await apiService.close(() {}); - await apiService.connect(); + await apiService.connect(force: true); await Future.delayed(Duration(seconds: 1)); }, child: ListView.builder( diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 4f73b7b..5a1ef30 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -8,6 +8,7 @@ import 'package:lottie/lottie.dart'; import 'package:no_screenshot/no_screenshot.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart'; +import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/animate_icon.dart'; @@ -252,13 +253,9 @@ class _MediaViewerViewState extends State { if ((imageBytes == null && !content.isVideo) || (content.isVideo && videoController == null)) { + Log.error("media files are not found..."); // When the message should be downloaded but imageBytes are null then a error happened - await twonlyDB.messagesDao.updateMessageByMessageId( - current.messageId, - MessagesCompanion( - errorWhileSending: Value(true), - ), - ); + await handleMediaError(current); return nextMediaOrExit(); } diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index 5bc1410..9399db6 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -68,7 +68,7 @@ class HomeViewState extends State { offsetRatio = offsetFromOne.abs(); }); } - if (cameraController == null && !initCameraStarted) { + if (cameraController == null && !initCameraStarted && offsetRatio < 1) { initCameraStarted = true; selectCamera(selectedCameraDetails.cameraId, false, false); } diff --git a/lib/src/views/settings/subscription/checkout.view.dart b/lib/src/views/settings/subscription/checkout.view.dart index 3b632f7..522a091 100644 --- a/lib/src/views/settings/subscription/checkout.view.dart +++ b/lib/src/views/settings/subscription/checkout.view.dart @@ -135,7 +135,7 @@ class _CheckoutViewState extends State { Navigator.pop(context); } }, - child: Text(context.lang.selectPaymentMethode)), + child: Text(context.lang.selectPaymentMethod)), ), SizedBox(height: 20) ], diff --git a/lib/src/views/settings/subscription/select_payment.view.dart b/lib/src/views/settings/subscription/select_payment.view.dart index 6710067..e7c6f8d 100644 --- a/lib/src/views/settings/subscription/select_payment.view.dart +++ b/lib/src/views/settings/subscription/select_payment.view.dart @@ -1,8 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; @@ -82,7 +80,7 @@ class _SelectPaymentViewState extends State { (balanceInCents == null || balanceInCents! >= checkoutInCents)); return Scaffold( appBar: AppBar( - title: Text(context.lang.selectPaymentMethode), + title: Text(context.lang.selectPaymentMethod), ), body: SafeArea( child: Column( @@ -222,21 +220,12 @@ class _SelectPaymentViewState extends State { widget.planId!, widget.payMonthly!, tryAutoRenewal); if (!context.mounted) return; if (res.isSuccess) { - context.read().plan = - widget.planId!; - var user = await getUser(); - if (user != null) { - user.subscriptionPlan = widget.planId!; - await updateUser(user); - } + await updateUsersPlan(context, widget.planId!); if (!context.mounted) return; - context - .read() - .updatePlan(widget.planId!); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: - Text(context.lang.planSuccessUpgraded)), + content: Text(context.lang.planSuccessUpgraded), + ), ); Navigator.of(context).pop(true); } else { diff --git a/lib/src/views/settings/subscription/subscription.view.dart b/lib/src/views/settings/subscription/subscription.view.dart index 0e3ecce..ca9b617 100644 --- a/lib/src/views/settings/subscription/subscription.view.dart +++ b/lib/src/views/settings/subscription/subscription.view.dart @@ -556,12 +556,12 @@ Future redeemUserInviteCode(BuildContext context, String newPlan) async { if (res.isSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.lang.redeemUserInviteCodeSuccess)), + content: Text(context.lang.redeemUserInviteCodeSuccess), + ), ); // reconnect to load new plan. - apiService.close(() { - apiService.connect(); - }); + await apiService.close(() {}); + await apiService.connect(); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(errorCodeToText(context, res.error))),