From 6d9df0328da4f0ca2044ee489421763878c7ddaf Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 16 Mar 2025 15:44:07 +0100 Subject: [PATCH] send multiple images with one key does work now --- android/app/build.gradle | 2 +- lib/main.dart | 2 +- .../components/message_send_state_icon.dart | 41 +- lib/src/database/database.dart | 23 +- lib/src/model/json/message.dart | 27 +- lib/src/proto/api/server_to_client.pb.dart | 16 +- .../proto/api/server_to_client.pbjson.dart | 7 +- lib/src/providers/api/api.dart | 390 +++--------------- lib/src/providers/api/media.dart | 319 ++++++++++++++ lib/src/providers/api/server_messages.dart | 129 ++++-- lib/src/providers/api_provider.dart | 13 +- lib/src/providers/hive.dart | 30 ++ .../share_image_editor_view.dart | 2 +- .../camera_to_share/share_image_view.dart | 12 +- .../views/chats/chat_item_details_view.dart | 14 +- lib/src/views/chats/chat_list_view.dart | 50 ++- lib/src/views/chats/media_viewer_view.dart | 78 ++-- lib/src/views/chats/search_username_view.dart | 3 + pubspec.lock | 56 ++- pubspec.yaml | 4 +- 20 files changed, 689 insertions(+), 529 deletions(-) create mode 100644 lib/src/providers/api/media.dart create mode 100644 lib/src/providers/hive.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 4d02e20..e551980 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ android { multiDexEnabled true // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = 23 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/lib/main.dart b/lib/main.dart index 9299fb2..52f31a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,11 +3,11 @@ import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/database.dart'; -import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api_provider.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:twonly/src/providers/db_provider.dart'; +import 'package:twonly/src/providers/hive.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/services/fcm_service.dart'; diff --git a/lib/src/components/message_send_state_icon.dart b/lib/src/components/message_send_state_icon.dart index 1c7da64..1985a27 100644 --- a/lib/src/components/message_send_state_icon.dart +++ b/lib/src/components/message_send_state_icon.dart @@ -57,28 +57,9 @@ class MessageSendStateIcon extends StatefulWidget { } class _MessageSendStateIconState extends State { - Message? videoMsg; - Message? textMsg; - Message? imageMsg; - @override void initState() { super.initState(); - - for (Message msg in widget.messages) { - if (msg.kind == MessageKind.textMessage) { - textMsg = msg; - } - if (msg.kind == MessageKind.media) { - MediaMessageContent content = - MediaMessageContent.fromJson(jsonDecode(msg.contentJson!)); - if (content.isVideo) { - videoMsg = msg; - } else { - imageMsg = msg; - } - } - } } Widget getLoaderIcon(color) { @@ -128,6 +109,15 @@ class _MessageSendStateIconState extends State { case MessageSendState.received: icon = Icon(Icons.square_rounded, size: 14, color: color); text = context.lang.messageSendState_Received; + if (message.kind == MessageKind.media) { + if (message.downloadState == DownloadState.pending) { + text = context.lang.messageSendState_TapToLoad; + } + if (message.downloadState == DownloadState.downloading) { + text = context.lang.messageSendState_Loading; + icon = getLoaderIcon(color); + } + } break; case MessageSendState.send: icon = @@ -135,21 +125,14 @@ class _MessageSendStateIconState extends State { text = context.lang.messageSendState_Send; break; case MessageSendState.sending: - case MessageSendState.receiving: icon = getLoaderIcon(color); text = context.lang.messageSendState_Sending; + case MessageSendState.receiving: + icon = getLoaderIcon(color); + text = context.lang.messageSendState_Received; break; } - if (message.kind == MessageKind.media) { - if (message.downloadState == DownloadState.pending) { - text = context.lang.messageSendState_TapToLoad; - } - if (message.downloadState == DownloadState.downloaded) { - text = context.lang.messageSendState_Loading; - icon = getLoaderIcon(color); - } - } icons.add(icon); } diff --git a/lib/src/database/database.dart b/lib/src/database/database.dart index 7a6ade1..f8c23fe 100644 --- a/lib/src/database/database.dart +++ b/lib/src/database/database.dart @@ -34,6 +34,16 @@ class TwonlyDatabase extends _$TwonlyDatabase { .watch(); } + Stream> watchMediaMessageNotOpened(int contactId) { + return (select(messages) + ..where((t) => + t.openedAt.isNull() & + t.contactId.equals(contactId) & + t.kind.equals(MessageKind.media.name)) + ..orderBy([(t) => OrderingTerm.desc(t.sendAt)])) + .watch(); + } + Stream> watchLastMessage(int contactId) { return (select(messages) ..where((t) => t.contactId.equals(contactId)) @@ -51,9 +61,12 @@ class TwonlyDatabase extends _$TwonlyDatabase { Future> getAllMessagesPendingDownloading() { return (select(messages) - ..where((t) => - t.downloadState.equals(DownloadState.downloaded.index).not() & - t.kind.equals(MessageKind.media.name))) + ..where( + (t) => + t.downloadState.equals(DownloadState.downloaded.index).not() & + t.messageOtherId.isNotNull() & + t.kind.equals(MessageKind.media.name), + )) .get(); } @@ -108,6 +121,10 @@ class TwonlyDatabase extends _$TwonlyDatabase { return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); } + SingleOrNullSelectable getMessageByMessageId(int messageId) { + return select(messages)..where((t) => t.messageId.equals(messageId)); + } + // ------------ Future insertContact(ContactsCompanion contact) async { diff --git a/lib/src/model/json/message.dart b/lib/src/model/json/message.dart index 641ce72..eb3aebe 100644 --- a/lib/src/model/json/message.dart +++ b/lib/src/model/json/message.dart @@ -104,20 +104,38 @@ class MessageContent { } class MediaMessageContent extends MessageContent { - final List downloadToken; final int maxShowTime; final bool isRealTwonly; final bool isVideo; + final List? downloadToken; + final List? encryptionKey; + final List? encryptionMac; + final List? encryptionNonce; + MediaMessageContent({ - required this.downloadToken, required this.maxShowTime, required this.isRealTwonly, required this.isVideo, + this.downloadToken, + this.encryptionKey, + this.encryptionMac, + this.encryptionNonce, }); static MediaMessageContent fromJson(Map json) { return MediaMessageContent( - downloadToken: List.from(json['downloadToken']), + downloadToken: json['downloadToken'] == null + ? null + : List.from(json['downloadToken']), + encryptionKey: json['encryptionKey'] == null + ? null + : List.from(json['encryptionKey']), + encryptionMac: json['encryptionMac'] == null + ? null + : List.from(json['encryptionMac']), + encryptionNonce: json['encryptionNonce'] == null + ? null + : List.from(json['encryptionNonce']), maxShowTime: json['maxShowTime'], isRealTwonly: json['isRealTwonly'], isVideo: json['isVideo'] ?? false, @@ -128,6 +146,9 @@ class MediaMessageContent extends MessageContent { Map toJson() { return { 'downloadToken': downloadToken, + 'encryptionKey': encryptionKey, + 'encryptionMac': encryptionMac, + 'encryptionNonce': encryptionNonce, 'isRealTwonly': isRealTwonly, 'maxShowTime': maxShowTime, }; diff --git a/lib/src/proto/api/server_to_client.pb.dart b/lib/src/proto/api/server_to_client.pb.dart index 799a420..3ee6b91 100644 --- a/lib/src/proto/api/server_to_client.pb.dart +++ b/lib/src/proto/api/server_to_client.pb.dart @@ -294,14 +294,14 @@ class NewMessage extends $pb.GeneratedMessage { class DownloadData extends $pb.GeneratedMessage { factory DownloadData({ - $core.List<$core.int>? uploadToken, + $core.List<$core.int>? downloadToken, $core.int? offset, $core.List<$core.int>? data, $core.bool? fin, }) { final $result = create(); - if (uploadToken != null) { - $result.uploadToken = uploadToken; + if (downloadToken != null) { + $result.downloadToken = downloadToken; } if (offset != null) { $result.offset = offset; @@ -319,7 +319,7 @@ class DownloadData extends $pb.GeneratedMessage { factory DownloadData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DownloadData', package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'uploadToken', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'downloadToken', $pb.PbFieldType.OY) ..a<$core.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3) ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) ..aOB(4, _omitFieldNames ? '' : 'fin') @@ -348,13 +348,13 @@ class DownloadData extends $pb.GeneratedMessage { static DownloadData? _defaultInstance; @$pb.TagNumber(1) - $core.List<$core.int> get uploadToken => $_getN(0); + $core.List<$core.int> get downloadToken => $_getN(0); @$pb.TagNumber(1) - set uploadToken($core.List<$core.int> v) { $_setBytes(0, v); } + set downloadToken($core.List<$core.int> v) { $_setBytes(0, v); } @$pb.TagNumber(1) - $core.bool hasUploadToken() => $_has(0); + $core.bool hasDownloadToken() => $_has(0); @$pb.TagNumber(1) - void clearUploadToken() => clearField(1); + void clearDownloadToken() => clearField(1); @$pb.TagNumber(2) $core.int get offset => $_getIZ(1); diff --git a/lib/src/proto/api/server_to_client.pbjson.dart b/lib/src/proto/api/server_to_client.pbjson.dart index 1bfa2d9..29aa9ed 100644 --- a/lib/src/proto/api/server_to_client.pbjson.dart +++ b/lib/src/proto/api/server_to_client.pbjson.dart @@ -72,7 +72,7 @@ final $typed_data.Uint8List newMessageDescriptor = $convert.base64Decode( const DownloadData$json = { '1': 'DownloadData', '2': [ - {'1': 'upload_token', '3': 1, '4': 1, '5': 12, '10': 'uploadToken'}, + {'1': 'download_token', '3': 1, '4': 1, '5': 12, '10': 'downloadToken'}, {'1': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'}, {'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'}, {'1': 'fin', '3': 4, '4': 1, '5': 8, '10': 'fin'}, @@ -81,8 +81,9 @@ const DownloadData$json = { /// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode( - 'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm' - 'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu'); + 'CgxEb3dubG9hZERhdGESJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4SFg' + 'oGb2Zmc2V0GAIgASgNUgZvZmZzZXQSEgoEZGF0YRgDIAEoDFIEZGF0YRIQCgNmaW4YBCABKAhS' + 'A2Zpbg=='); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index 176c677..9fb0992 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -1,82 +1,59 @@ import 'dart:convert'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart'; import 'package:hive/hive.dart'; import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:twonly/globals.dart'; -import 'package:twonly/src/app.dart'; import 'package:twonly/src/database/database.dart'; import 'package:twonly/src/database/messages_db.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; -import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/providers/hive.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; - -Future tryDownloadAllMediaFiles() async { - - if (!await isAllowedToDownload()) { - return; - } - List messages = - await twonlyDatabase.getAllMessagesPendingDownloading(); - - for (Message message in messages) { - MessageContent? content = MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); - - if (content is MediaMessageContent) { - tryDownloadMedia(message.messageId, message.contactId, content.downloadToken); - } - } - -} - Future tryTransmitMessages() async { - List retransmit = - await twonlyDatabase.getAllMessagesForRetransmitting(); + // List retransmit = + // await twonlyDatabase.getAllMessagesForRetransmitting(); + // if (retransmit.isEmpty) return; - if (retransmit.isEmpty) return; + // Logger("api.dart").info("try sending messages: ${retransmit.length}"); - Logger("api.dart").info("try sending messages: ${retransmit.length}"); + // Box box = await getMediaStorage(); + // for (int i = 0; i < retransmit.length; i++) { + // int msgId = retransmit[i].messageId; - Box box = await getMediaStorage(); - for (int i = 0; i < retransmit.length; i++) { - int msgId = retransmit[i].messageId; + // Uint8List? bytes = box.get("retransmit-$msgId-textmessage"); + // if (bytes != null) { + // Result resp = await apiProvider.sendTextMessage( + // retransmit[i].contactId, + // bytes, + // ); - Uint8List? bytes = box.get("retransmit-$msgId-textmessage"); - if (bytes != null) { - Result resp = await apiProvider.sendTextMessage( - retransmit[i].contactId, - bytes, - ); + // if (resp.isSuccess) { + // await twonlyDatabase.updateMessageByMessageId( + // msgId, MessagesCompanion(acknowledgeByServer: Value(true))); - if (resp.isSuccess) { - await twonlyDatabase.updateMessageByMessageId( - msgId, - MessagesCompanion(acknowledgeByServer: Value(true)) - ); + // box.delete("retransmit-$msgId-textmessage"); + // } else { + // // in case of error do nothing. As the message is not removed the app will try again when relaunched + // } + // } - box.delete("retransmit-$msgId-textmessage"); - } else { - // in case of error do nothing. As the message is not removed the app will try again when relaunched - } - } - - Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media"); - if (encryptedMedia != null) { - MediaMessageContent content = MediaMessageContent.fromJson(jsonDecode(retransmit[i].contentJson!)); - uploadMediaFile(msgId, retransmit[i].contactId, encryptedMedia, - content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt); - } - } + // Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media"); + // if (encryptedMedia != null) { + // MediaMessageContent content = + // MediaMessageContent.fromJson(jsonDecode(retransmit[i].contentJson!)); + // uploadMediaFile(msgId, retransmit[i].contactId, encryptedMedia, + // content.isRealTwonly, content.maxShowTime, retransmit[i].sendAt); + // } + // } } // this functions ensures that the message is received by the server and in case of errors will try again later -Future encryptAndSendMessage(int userId, MessageJson msg) async { +Future encryptAndSendMessage( + int? messageId, int userId, MessageJson msg) async { Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId); if (bytes == null) { @@ -85,21 +62,19 @@ Future encryptAndSendMessage(int userId, MessageJson msg) async { } Box box = await getMediaStorage(); - if (msg.messageId != null) { - box.put("retransmit-${msg.messageId}-textmessage", bytes); + if (messageId != null) { + box.put("retransmit-$messageId-textmessage", bytes); } Result resp = await apiProvider.sendTextMessage(userId, bytes); if (resp.isSuccess) { - if (msg.messageId != null) { - - - await twonlyDatabase.updateMessageByMessageId( - msg.messageId!, - MessagesCompanion(acknowledgeByServer: Value(true)) - ); - box.delete("retransmit-${msg.messageId}-textmessage"); + if (messageId != null) { + await twonlyDatabase.updateMessageByMessageId( + messageId, + MessagesCompanion(acknowledgeByServer: Value(true)), + ); + box.delete("retransmit-$messageId-textmessage"); } } @@ -111,14 +86,14 @@ Future sendTextMessage(int target, String message) async { DateTime messageSendAt = DateTime.now(); - int? messageId = await twonlyDatabase.insertMessage(MessagesCompanion( - contactId: Value(target), - kind: Value(MessageKind.textMessage), - sendAt: Value(messageSendAt), - downloadState: Value(DownloadState.downloaded), - contentJson: Value(jsonEncode(content.toJson())) - ),); - + int? messageId = await twonlyDatabase.insertMessage( + MessagesCompanion( + contactId: Value(target), + kind: Value(MessageKind.textMessage), + sendAt: Value(messageSendAt), + downloadState: Value(DownloadState.downloaded), + contentJson: Value(jsonEncode(content.toJson()))), + ); if (messageId == null) return; @@ -129,226 +104,15 @@ Future sendTextMessage(int target, String message) async { timestamp: messageSendAt, ); - encryptAndSendMessage(target, msg); + encryptAndSendMessage(messageId, target, msg); } -// this will send the media file and ensures retransmission when errors occur -Future uploadMediaFile( - int messageId, - int target, - Uint8List encryptedMedia, - bool isRealTwonly, - int maxShowTime, - DateTime messageSendAt, -) async { - Box box = await getMediaStorage(); - Logger("api.dart").info("Uploading image $messageId"); - - List? uploadToken = box.get("retransmit-$messageId-uploadtoken"); - if (uploadToken == null) { - Result res = await apiProvider.getUploadToken(); - - if (res.isError || !res.value.hasUploadtoken()) { - Logger("api.dart").shout("Error getting upload token!"); - return; // will be retried on next app start - } - - uploadToken = res.value.uploadtoken; - } - - if (uploadToken == null) return; - - int offset = box.get("retransmit-$messageId-offset") ?? 0; - - int fragmentedTransportSize = 1_000_000; // per upload transfer - - while (offset < encryptedMedia.length) { - Logger("api.dart").info("Uploading image $messageId with offset: $offset"); - int end = encryptedMedia.length; - if (offset + fragmentedTransportSize < encryptedMedia.length) { - end = offset + fragmentedTransportSize; - } - - Result wasSend = await apiProvider.uploadData( - uploadToken, - encryptedMedia.sublist(offset, end), - offset, - ); - - if (wasSend.isError) { - await box.put("retransmit-$messageId-offset", 0); - await box.delete("retransmit-$messageId-uploadtoken"); - Logger("api.dart").shout("error while uploading media"); - return; - } - - box.put("retransmit-$messageId-offset", offset); - - offset = end; - } - - box.delete("retransmit-$messageId-media"); - box.delete("retransmit-$messageId-uploadtoken"); - - twonlyDatabase.incTotalMediaCounter(target); - twonlyDatabase.updateContact( - target, - ContactsCompanion( - lastMessageReceived: Value(messageSendAt), - ), - ); - - // Ensures the retransmit of the message - await encryptAndSendMessage( - target, - MessageJson( - kind: MessageKind.media, - messageId: messageId, - content: MediaMessageContent( - downloadToken: uploadToken, - maxShowTime: maxShowTime, - isRealTwonly: isRealTwonly, - isVideo: false), - timestamp: messageSendAt, - ), - ); -} - -class SendImage { - final int userId; - final Uint8List imageBytes; - final bool isRealTwonly; - final int maxShowTime; - DateTime? messageSendAt; - int? messageId; - Uint8List? encryptBytes; - - SendImage({ - required this.userId, - required this.imageBytes, - required this.isRealTwonly, - required this.maxShowTime, - }); - - Future upload() async { - if (messageId == null || encryptBytes == null || messageSendAt == null) { - return; - } - await uploadMediaFile(messageId!, userId, encryptBytes!, isRealTwonly, - maxShowTime, messageSendAt!); - } - - Future encryptAndStore() async { - encryptBytes = await SignalHelper.encryptBytes(imageBytes, userId); - if (encryptBytes == null) { - Logger("api.dart").shout("Error encrypting media! Aborting"); - return; - } - - messageSendAt = DateTime.now(); - int? messageId = await twonlyDatabase.insertMessage(MessagesCompanion( - contactId: Value(userId), - kind: Value(MessageKind.media), - sendAt: Value(messageSendAt!), - downloadState: Value(DownloadState.pending), - contentJson: Value(jsonEncode(MediaMessageContent( - downloadToken: [], - maxShowTime: maxShowTime, - isRealTwonly: isRealTwonly, - isVideo: false, - ).toJson())) - )); - - // should only happen when there is no space left on the smartphone -> abort message - if (messageId == null) return; - - Box box = await getMediaStorage(); - await box.put("retransmit-$messageId-media", encryptBytes); - // message is safe until now -> would be retransmitted if sending would fail.. - } -} - -Future sendImage( - List userIds, - Uint8List imageBytes, - bool isRealTwonly, - int maxShowTime, -) async { - Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes); - if (imageBytesCompressed == null) { - Logger("api.dart").shout("Error compressing image!"); - return; - } - - if (imageBytesCompressed.length >= 10000000) { - Logger("api.dart").shout("Image to big aborting!"); - return; - } - - List tasks = []; - - for (int i = 0; i < userIds.length; i++) { - tasks.add( - SendImage( - userId: userIds[i], - imageBytes: imageBytesCompressed, - isRealTwonly: isRealTwonly, - maxShowTime: maxShowTime, - ), - ); - } - - // first step encrypt and store the encrypted image - await Future.wait(tasks.map((task) => task.encryptAndStore())); - - // after the images are safely stored try do upload them one by one - for (SendImage task in tasks) { - task.upload(); - } -} - -Future tryDownloadMedia(int messageId, int fromUserId, List mediaToken, - {bool force = false}) async { - if (globalIsAppInBackground) return; - - if (!force) { - // TODO: create option to enable download via mobile data - final List connectivityResult = - await (Connectivity().checkConnectivity()); - if (connectivityResult.contains(ConnectivityResult.mobile)) { - Logger("tryDownloadMedia").info("abort download over mobile connection"); - return; - } - } - - final box = await getMediaStorage(); - if (box.containsKey("${mediaToken}_downloaded")) { - Logger("tryDownloadMedia").shout("mediaToken already downloaded"); - return; - } - - Logger("tryDownloadMedia").info("Downloading: $mediaToken"); - int offset = 0; - Uint8List? media = box.get("$mediaToken"); - if (media != null && media.isNotEmpty) { - offset = media.length; - } - box.put("${mediaToken}_messageId", messageId); - box.put("${mediaToken}_fromUserId", fromUserId); - final update = - MessagesCompanion(downloadState: Value(DownloadState.downloading)); - await twonlyDatabase.updateMessageByOtherUser( - fromUserId, - messageId, - update - ); - apiProvider.triggerDownload(mediaToken, offset); -} - -Future notifyContactAboutOpeningMessage(int fromUserId, int messageOtherId) async { +Future notifyContactAboutOpeningMessage( + int fromUserId, int messageOtherId) async { //await DbMessages.userOpenedOtherMessage(fromUserId, messageOtherId); encryptAndSendMessage( + null, fromUserId, MessageJson( kind: MessageKind.opened, @@ -358,53 +122,3 @@ Future notifyContactAboutOpeningMessage(int fromUserId, int messageOtherId) asyn ), ); } - -Future getDownloadedMedia( - List mediaToken, int messageOtherId, int otherUserId) async { - final box = await getMediaStorage(); - Uint8List? media; - try { - media = box.get("${mediaToken}_downloaded"); - } catch (e) { - return null; - } - if (media == null) return null; - - // await userOpenedOtherMessage(otherUserId, messageOtherId); - notifyContactAboutOpeningMessage(otherUserId, messageOtherId); - twonlyDatabase.updateMessageByOtherMessageId(otherUserId, messageOtherId, MessagesCompanion( - openedAt: Value(DateTime.now()) - )); - - box.delete(mediaToken.toString()); - box.put("${mediaToken}_downloaded", "deleted"); - box.delete("${mediaToken}_messageId"); - box.delete("${mediaToken}_fromUserId"); - return media; -} - -Future initMediaStorage() async { - final storage = getSecureStorage(); - var containsEncryptionKey = - await storage.containsKey(key: 'hive_encryption_key'); - if (!containsEncryptionKey) { - var key = Hive.generateSecureKey(); - await storage.write( - key: 'hive_encryption_key', - value: base64UrlEncode(key), - ); - } - final dir = await getApplicationDocumentsDirectory(); - Hive.init(dir.path); -} - -Future getMediaStorage() async { - await initMediaStorage(); - - final storage = getSecureStorage(); - var encryptionKey = - base64Url.decode((await storage.read(key: 'hive_encryption_key'))!); - - return await Hive.openBox('media_storage', - encryptionCipher: HiveAesCipher(encryptionKey)); -} diff --git a/lib/src/providers/api/media.dart b/lib/src/providers/api/media.dart new file mode 100644 index 0000000..50e2602 --- /dev/null +++ b/lib/src/providers/api/media.dart @@ -0,0 +1,319 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'package:cryptography_plus/cryptography_plus.dart'; +import 'package:drift/drift.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/app.dart'; +import 'package:twonly/src/database/database.dart'; +import 'package:twonly/src/database/messages_db.dart'; +import 'package:twonly/src/model/json/message.dart'; +import 'package:twonly/src/proto/api/server_to_client.pb.dart'; +import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:twonly/src/providers/hive.dart'; +import 'package:twonly/src/utils/misc.dart'; + +Future tryDownloadAllMediaFiles() async { + if (!await isAllowedToDownload()) { + return; + } + List messages = + await twonlyDatabase.getAllMessagesPendingDownloading(); + + for (Message message in messages) { + MessageContent? content = + MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); + + if (content is MediaMessageContent) { + tryDownloadMedia( + message.messageId, + message.contactId, + content, + ); + } + } +} + +class Metadata { + late List userIds; + late HashMap messageIds; + late Uint8List imageBytes; + late bool isRealTwonly; + late int maxShowTime; + late DateTime messageSendAt; +} + +class PrepareState { + late List sha2Hash; + late List encryptionKey; + late List encryptionMac; + late List encryptedBytes; + late List encryptionNonce; +} + +class UploadState { + late List uploadToken; + late List> downloadTokens; +} + +class ImageUploader { + static Future prepareState(Uint8List imageBytes) async { + Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes); + if (imageBytesCompressed == null) { + // non recoverable state + Logger("media.dart").shout("Error compressing image!"); + return null; + } + + if (imageBytesCompressed.length >= 10000000) { + // non recoverable state + Logger("media.dart").shout("Image to big aborting!"); + return null; + } + + var state = PrepareState(); + + try { + final xchacha20 = Xchacha20.poly1305Aead(); + SecretKeyData secretKey = + await (await xchacha20.newSecretKey()).extract(); + + state.encryptionKey = secretKey.bytes; + state.encryptionNonce = xchacha20.newNonce(); + + final secretBox = await xchacha20.encrypt( + imageBytesCompressed, + secretKey: secretKey, + nonce: state.encryptionNonce, + ); + + state.encryptionMac = secretBox.mac.bytes; + + state.encryptedBytes = secretBox.cipherText; + final algorithm = Sha256(); + state.sha2Hash = (await algorithm.hash(state.encryptedBytes)).bytes; + + return state; + } catch (e) { + Logger("media.dart").shout("Error encrypting image: $e"); + // non recoverable state + return null; + } + } + + static Future uploadState( + PrepareState prepareState, int recipientsCount) async { + int fragmentedTransportSize = 1000000; // per upload transfer + + final res = await apiProvider.getUploadToken(recipientsCount); + + if (res.isError || !res.value.hasUploadtoken()) { + Logger("media.dart").shout("Error getting upload token!"); + return null; // will be retried on next app start + } + + Response_UploadToken tokens = res.value.uploadtoken; + + var state = UploadState(); + state.uploadToken = tokens.uploadToken; + state.downloadTokens = tokens.downloadTokens; + + // box.get("retransmit-$messageId-offset", offset) + int offset = 0; + + while (offset < prepareState.encryptedBytes.length) { + Logger("api.dart").info( + "Uploading image ${prepareState.encryptionMac} with offset: $offset"); + + int end; + List? checksum; + if (offset + fragmentedTransportSize < + prepareState.encryptedBytes.length) { + end = offset + fragmentedTransportSize; + } else { + end = prepareState.encryptedBytes.length; + checksum = prepareState.sha2Hash; + } + + Result wasSend = await apiProvider.uploadData( + state.uploadToken, + Uint8List.fromList(prepareState.encryptedBytes.sublist(offset, end)), + offset, + checksum, + ); + + if (wasSend.isError) { + // await box.put("retransmit-$messageId-offset", 0); + // await box.delete("retransmit-$messageId-uploadtoken"); + Logger("api.dart").shout("error while uploading media"); + return null; + } + offset = end; + } + return state; + } + + static Future notifyState(PrepareState prepareState, UploadState uploadState, + Metadata metadata) async { + for (int targetUserId in metadata.userIds) { + // should never happen + if (uploadState.downloadTokens.isEmpty) return; + if (!metadata.messageIds.containsKey(targetUserId)) continue; + + final downloadToken = uploadState.downloadTokens.removeLast(); + + twonlyDatabase.incTotalMediaCounter(targetUserId); + twonlyDatabase.updateContact( + targetUserId, + ContactsCompanion( + lastMessageReceived: Value(metadata.messageSendAt), + ), + ); + + // Ensures the retransmit of the message + encryptAndSendMessage( + metadata.messageIds[targetUserId], + targetUserId, + MessageJson( + kind: MessageKind.media, + messageId: metadata.messageIds[targetUserId], + content: MediaMessageContent( + downloadToken: downloadToken, + maxShowTime: metadata.maxShowTime, + isRealTwonly: metadata.isRealTwonly, + isVideo: false, + encryptionKey: prepareState.encryptionKey, + encryptionMac: prepareState.encryptionMac, + encryptionNonce: prepareState.encryptionNonce, + ), + timestamp: metadata.messageSendAt, + ), + ); + } + } +} + +Future sendImage( + List userIds, + Uint8List imageBytes, + bool isRealTwonly, + int maxShowTime, +) async { + final prepareState = await ImageUploader.prepareState(imageBytes); + if (prepareState == null) { + // non recoverable state + return; + } + + var metadata = Metadata(); + metadata.userIds = userIds; + metadata.isRealTwonly = isRealTwonly; + metadata.maxShowTime = maxShowTime; + metadata.messageIds = HashMap(); + metadata.messageSendAt = DateTime.now(); + + // store prepareState and metadata... + + // at this point it is safe inform the user about the process of sending the image.. + for (final userId in metadata.userIds) { + int? messageId = await twonlyDatabase.insertMessage( + MessagesCompanion( + contactId: Value(userId), + kind: Value(MessageKind.media), + sendAt: Value(metadata.messageSendAt), + downloadState: Value(DownloadState.pending), + contentJson: Value( + jsonEncode( + MediaMessageContent( + maxShowTime: metadata.maxShowTime, + isRealTwonly: metadata.isRealTwonly, + isVideo: false, + ).toJson(), + ), + ), + ), + ); + if (messageId != null) { + metadata.messageIds[userId] = messageId; + } else { + Logger("media.dart") + .shout("Error inserting message in messages database..."); + } + } + + final uploadState = + await ImageUploader.uploadState(prepareState, metadata.userIds.length); + if (uploadState == null) { + return; + } + + // delete prepareState and store uploadState... + + final notifyState = + await ImageUploader.notifyState(prepareState, uploadState, metadata); + if (notifyState == null) { + return; + } +} + +Future tryDownloadMedia( + int messageId, int fromUserId, MediaMessageContent content, + {bool force = false}) async { + if (globalIsAppInBackground) return; + if (content.downloadToken == null) return; + + if (!force) { + if (!await isAllowedToDownload()) { + Logger("tryDownloadMedia").info("abort download over mobile connection"); + return; + } + } + + final box = await getMediaStorage(); + if (box.containsKey("${messageId}_downloaded")) { + Logger("tryDownloadMedia").shout("mediaToken already downloaded"); + return; + } + + Logger("tryDownloadMedia").info("Downloading: $messageId"); + + int offset = 0; + Uint8List? media = box.get("${content.downloadToken!}"); + if (media != null && media.isNotEmpty) { + offset = media.length; + } + + box.put("${content.downloadToken!}_messageId", messageId); + + await twonlyDatabase.updateMessageByOtherUser( + fromUserId, + messageId, + MessagesCompanion( + downloadState: Value(DownloadState.downloading), + ), + ); + apiProvider.triggerDownload(content.downloadToken!, offset); +} + +Future getDownloadedMedia( + Message message, List downloadToken) async { + final box = await getMediaStorage(); + Uint8List? media; + try { + media = box.get("${downloadToken}_downloaded"); + } catch (e) { + return null; + } + if (media == null) return null; + + // await userOpenedOtherMessage(otherUserId, messageOtherId); + notifyContactAboutOpeningMessage(message.contactId, message.messageOtherId!); + twonlyDatabase.updateMessageByMessageId( + message.messageId, MessagesCompanion(openedAt: Value(DateTime.now()))); + + box.delete(downloadToken.toString()); + box.put("${downloadToken}_downloaded", "deleted"); + box.delete("${downloadToken}_messageId"); + return media; +} diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index b5969ca..7d1f068 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:drift/drift.dart'; import 'package:fixnum/fixnum.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; @@ -16,6 +17,8 @@ import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/proto/api/server_to_client.pbserver.dart'; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:twonly/src/providers/api/media.dart'; +import 'package:twonly/src/providers/hive.dart'; import 'package:twonly/src/services/notification_service.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; @@ -53,26 +56,31 @@ Future handleDownloadData(DownloadData data) async { // download should only be done when the app is open return client.Response()..error = ErrorCode.InternalError; } + Logger("server_messages") - .info("downloading: ${data.uploadToken} ${data.fin}"); + .info("downloading: ${data.downloadToken} ${data.fin}"); + final box = await getMediaStorage(); - String boxId = data.uploadToken.toString(); - int? messageId = box.get("${data.uploadToken}_messageId"); + String boxId = data.downloadToken.toString(); + + int? messageId = box.get("${data.downloadToken}_messageId"); + + if (messageId == null) { + Logger("server_messages") + .info("download data received, but unknown messageID"); + // answers with ok, so the server will delete the message + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; + } + if (data.fin && data.data.isEmpty) { // media file was deleted by the server. remove the media from device - - if (messageId != null) { - await twonlyDatabase.deleteMessageById(messageId); - box.delete(boxId); - box.delete("${data.uploadToken}_fromUserId"); - box.delete("${data.uploadToken}_downloaded"); - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } else { - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } + await twonlyDatabase.deleteMessageById(messageId); + box.delete(boxId); + box.delete("${data.downloadToken}_downloaded"); + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; } Uint8List? buffered = box.get(boxId); @@ -93,34 +101,60 @@ Future handleDownloadData(DownloadData data) async { downloadedBytes = Uint8List.fromList(data.data); } - if (data.fin) { - SignalHelper.getSignalStore(); - int? fromUserId = box.get("${data.uploadToken}_fromUserId"); - if (fromUserId != null) { - Uint8List? rawBytes = - await SignalHelper.decryptBytes(downloadedBytes, fromUserId); - - if (rawBytes != null) { - box.put("${data.uploadToken}_downloaded", rawBytes); - } else { - Logger("server_messages") - .shout("error decrypting the message: ${data.uploadToken}"); - } - - final update = - MessagesCompanion(downloadState: Value(DownloadState.downloaded)); - await twonlyDatabase.updateMessageByOtherUser( - fromUserId, - messageId!, - update, - ); - - box.delete(boxId); - } - } else { + if (!data.fin) { + // download not finished, so waiting for more data... box.put(boxId, downloadedBytes); + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; } + // Uint8List? rawBytes = + // await SignalHelper.decryptBytes(downloadedBytes, fromUserId); + + Message? msg = + await twonlyDatabase.getMessageByMessageId(messageId).getSingleOrNull(); + if (msg == null) { + Logger("server_messages") + .info("messageId not found in database. Ignoring download"); + // answers with ok, so the server will delete the message + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; + } + + MediaMessageContent content = + MediaMessageContent.fromJson(jsonDecode(msg.contentJson!)); + + final xchacha20 = Xchacha20.poly1305Aead(); + SecretKeyData secretKeyData = SecretKeyData(content.encryptionKey!); + + SecretBox secretBox = SecretBox( + downloadedBytes, + nonce: content.encryptionNonce!, + mac: Mac(content.encryptionMac!), + ); + + try { + final rawBytes = + await xchacha20.decrypt(secretBox, secretKey: secretKeyData); + + box.put("${data.downloadToken}_downloaded", rawBytes); + } catch (e) { + Logger("server_messages").info("Decryption error: $e"); + // deleting message as this is an invalid image + await twonlyDatabase.deleteMessageById(messageId); + // answers with ok, so the server will delete the message + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; + } + + await twonlyDatabase.updateMessageByOtherUser( + msg.contactId, + messageId, + MessagesCompanion(downloadState: Value(DownloadState.downloaded)), + ); + + box.delete(boxId); + var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; } @@ -185,8 +219,11 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { kind: Value(message.kind), messageOtherId: Value(message.messageId), contentJson: Value(content), - downloadState: Value(DownloadState.downloaded), - sendAt: Value(message.timestamp), + acknowledgeByServer: Value(true), + downloadState: Value(message.kind == MessageKind.media + ? DownloadState.pending + : DownloadState.downloaded), + sendAt: Value(message.timestamp.toUtc()), ); final messageId = await twonlyDatabase.insertMessage( @@ -198,6 +235,7 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { } encryptAndSendMessage( + message.messageId!, fromUserId, MessageJson( kind: MessageKind.ack, @@ -220,8 +258,11 @@ Future handleNewMessage(int fromUserId, Uint8List body) async { if (!globalIsAppInBackground) { final content = message.content; if (content is MediaMessageContent) { - List downloadToken = content.downloadToken; - tryDownloadMedia(messageId, fromUserId, downloadToken); + tryDownloadMedia( + messageId, + fromUserId, + content, + ); } } } diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index 1664e54..e935525 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -11,6 +11,7 @@ import 'package:twonly/src/proto/api/error.pb.dart'; import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/api/server_messages.dart'; import 'package:twonly/src/services/fcm_service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -327,8 +328,9 @@ class ApiProvider { return await sendRequestSync(req); } - Future getUploadToken() async { - var get = ApplicationData_GetUploadToken(); + Future getUploadToken(int recipientsCount) async { + var get = ApplicationData_GetUploadToken() + ..recipientsCount = recipientsCount; var appData = ApplicationData()..getuploadtoken = get; var req = createClientToServerFromApplicationData(appData); return await sendRequestSync(req); @@ -343,12 +345,15 @@ class ApiProvider { return await sendRequestSync(req); } - Future uploadData( - List uploadToken, Uint8List data, int offset) async { + Future uploadData(List uploadToken, Uint8List data, int offset, + List? checksum) async { var get = ApplicationData_UploadData() ..uploadToken = uploadToken ..data = data ..offset = offset; + if (checksum != null) { + get.checksum = checksum; + } var appData = ApplicationData()..uploaddata = get; var req = createClientToServerFromApplicationData(appData); final result = await sendRequestSync(req); diff --git a/lib/src/providers/hive.dart b/lib/src/providers/hive.dart new file mode 100644 index 0000000..f0effa9 --- /dev/null +++ b/lib/src/providers/hive.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:twonly/src/utils/misc.dart'; + +Future initMediaStorage() async { + final storage = getSecureStorage(); + var containsEncryptionKey = + await storage.containsKey(key: 'hive_encryption_key'); + if (!containsEncryptionKey) { + var key = Hive.generateSecureKey(); + await storage.write( + key: 'hive_encryption_key', + value: base64UrlEncode(key), + ); + } + final dir = await getApplicationDocumentsDirectory(); + Hive.init(dir.path); +} + +Future getMediaStorage() async { + await initMediaStorage(); + + final storage = getSecureStorage(); + var encryptionKey = + base64Url.decode((await storage.read(key: 'hive_encryption_key'))!); + + return await Hive.openBox('media_storage', + encryptionCipher: HiveAesCipher(encryptionKey)); +} diff --git a/lib/src/views/camera_to_share/share_image_editor_view.dart b/lib/src/views/camera_to_share/share_image_editor_view.dart index 12d3885..53c6fd6 100644 --- a/lib/src/views/camera_to_share/share_image_editor_view.dart +++ b/lib/src/views/camera_to_share/share_image_editor_view.dart @@ -7,7 +7,7 @@ import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/components/notification_badge.dart'; import 'package:twonly/src/database/contacts_db.dart'; import 'package:twonly/src/database/database.dart'; -import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; diff --git a/lib/src/views/camera_to_share/share_image_view.dart b/lib/src/views/camera_to_share/share_image_view.dart index 3591ed9..cc84ed2 100644 --- a/lib/src/views/camera_to_share/share_image_view.dart +++ b/lib/src/views/camera_to_share/share_image_view.dart @@ -12,7 +12,7 @@ import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/components/verified_shield.dart'; import 'package:twonly/src/database/contacts_db.dart'; import 'package:twonly/src/database/database.dart'; -import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/home_view.dart'; @@ -65,8 +65,12 @@ class _ShareImageView extends State { //_users = await DbContacts.getActiveUsers(); // _updateUsers(_users); - // imageBytes = await widget.imageBytesFuture; - // setState(() {}); + initAsync(); + } + + Future initAsync() async { + imageBytes = await widget.imageBytesFuture; + setState(() {}); } @override @@ -219,7 +223,7 @@ class _ShareImageView extends State { setState(() { sendingImage = true; }); - await sendImage( + sendImage( _selectedUserIds.toList(), imageBytes!, widget.isRealTwonly, diff --git a/lib/src/views/chats/chat_item_details_view.dart b/lib/src/views/chats/chat_item_details_view.dart index 68264d2..6e35203 100644 --- a/lib/src/views/chats/chat_item_details_view.dart +++ b/lib/src/views/chats/chat_item_details_view.dart @@ -14,6 +14,7 @@ import 'package:twonly/src/database/database.dart'; import 'package:twonly/src/database/messages_db.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart'; @@ -31,19 +32,10 @@ class ChatListEntry extends StatelessWidget { @override Widget build(BuildContext context) { bool right = message.messageOtherId == null; - MessageSendState state = messageSendStateFromMessage(message); - - bool isDownloading = false; - List token = []; MessageContent? content = MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!)); - if (message.messageOtherId != null && content is MediaMessageContent) { - token = content.downloadToken; - isDownloading = message.downloadState == DownloadState.downloading; - } - Widget child = Container(); if (content is TextMessageContent) { @@ -86,7 +78,7 @@ class ChatListEntry extends StatelessWidget { child = GestureDetector( onTap: () { - if (state == MessageSendState.received && !isDownloading) { + if (message.kind == MessageKind.media) { if (message.downloadState == DownloadState.downloaded) { Navigator.push( context, @@ -95,7 +87,7 @@ class ChatListEntry extends StatelessWidget { }), ); } else { - tryDownloadMedia(message.messageId, message.contactId, token, + tryDownloadMedia(message.messageId, message.contactId, content, force: true); } } diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index 22c7100..08895fc 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; @@ -12,7 +13,7 @@ import 'package:twonly/src/database/contacts_db.dart'; import 'package:twonly/src/database/database.dart'; import 'package:twonly/src/database/messages_db.dart'; import 'package:twonly/src/model/json/message.dart'; -import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_item_details_view.dart'; @@ -151,7 +152,6 @@ class UserListItem extends StatefulWidget { class _UserListItem extends State { int lastMessageInSeconds = 0; MessageSendState state = MessageSendState.send; - List token = []; Message? currentMessage; Timer? updateTime; @@ -209,7 +209,14 @@ class _UserListItem extends State { var lastMessages = [lastMessage]; if (notOpenedMessagesSnapshot.data != null && notOpenedMessagesSnapshot.data!.isNotEmpty) { - lastMessages = notOpenedMessagesSnapshot.data!; + // filter first for only received messages + lastMessages = notOpenedMessagesSnapshot.data! + .where((x) => x.messageOtherId != null) + .toList(); + if (lastMessages.isEmpty) { + lastMessages = notOpenedMessagesSnapshot.data!; + } + var media = lastMessages.where((x) => x.kind == MessageKind.media); if (media.isNotEmpty) { @@ -253,22 +260,27 @@ class _UserListItem extends State { return; } Message msg = currentMessage!; - if (msg.downloadState == DownloadState.downloading) { - return; - } - if (msg.downloadState == DownloadState.pending) { - tryDownloadMedia(msg.messageId, msg.contactId, token, force: true); - return; - } - if (state == MessageSendState.received && - msg.kind == MessageKind.media) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return MediaViewerView(widget.user.userId); - }), - ); - return; + if (msg.kind == MessageKind.media && msg.messageOtherId != null) { + switch (msg.downloadState) { + case DownloadState.pending: + MediaMessageContent content = + MediaMessageContent.fromJson(jsonDecode(msg.contentJson!)); + tryDownloadMedia(msg.messageId, msg.contactId, content, + force: true); + return; + + case DownloadState.downloaded: + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return MediaViewerView(widget.user.userId); + }), + ); + return; + + default: + return; + } } Navigator.push( context, diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index 0e84295..0002839 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -13,6 +13,7 @@ import 'package:twonly/src/database/database.dart'; import 'package:twonly/src/database/messages_db.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/providers/api/api.dart'; +import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/misc.dart'; @@ -51,13 +52,12 @@ class _MediaViewerViewState extends State { void initState() { super.initState(); - asyncLoadNextMedia(); - loadCurrentMediaFile(); + asyncLoadNextMedia(true); } - Future asyncLoadNextMedia() async { + Future asyncLoadNextMedia(bool firstRun) async { Stream> messages = - twonlyDatabase.watchMessageNotOpened(widget.userId); + twonlyDatabase.watchMediaMessageNotOpened(widget.userId); _subscription = messages.listen((messages) { for (Message msg in messages) { @@ -66,6 +66,10 @@ class _MediaViewerViewState extends State { } } setState(() {}); + if (firstRun) { + loadCurrentMediaFile(); + firstRun = false; + } }); } @@ -117,42 +121,38 @@ class _MediaViewerViewState extends State { return; } } - flutterLocalNotificationsPlugin.cancel(current.messageId); - if (current.downloadState == DownloadState.pending) { - setState(() { - isDownloading = true; - }); - await tryDownloadMedia( - current.messageId, current.contactId, content.downloadToken, - force: true); - } - do { - if (isDownloading) { - await Future.delayed(Duration(milliseconds: 10)); - } - imageBytes = await getDownloadedMedia( - content.downloadToken, - current.messageOtherId!, - current.contactId, - ); - } while (isDownloading && imageBytes == null); - - isDownloading = false; - - if (imageBytes == null) { - nextMediaOrExit(); - return; - } - - if (content.maxShowTime != 999999) { - canBeSeenUntil = DateTime.now().add( - Duration(seconds: content.maxShowTime), - ); - maxShowTime = content.maxShowTime; - startTimer(); - } - setState(() {}); } + flutterLocalNotificationsPlugin.cancel(current.messageId); + if (current.downloadState == DownloadState.pending) { + setState(() { + isDownloading = true; + }); + await tryDownloadMedia(current.messageId, current.contactId, content, + force: true); + } + do { + if (isDownloading) { + await Future.delayed(Duration(milliseconds: 10)); + } + if (content.downloadToken == null) break; + imageBytes = await getDownloadedMedia(current, content.downloadToken!); + } while (isDownloading && imageBytes == null); + + isDownloading = false; + + if (imageBytes == null) { + nextMediaOrExit(); + return; + } + + if (content.maxShowTime != 999999) { + canBeSeenUntil = DateTime.now().add( + Duration(seconds: content.maxShowTime), + ); + maxShowTime = content.maxShowTime; + startTimer(); + } + setState(() {}); } startTimer() { diff --git a/lib/src/views/chats/search_username_view.dart b/lib/src/views/chats/search_username_view.dart index 1521120..178a104 100644 --- a/lib/src/views/chats/search_username_view.dart +++ b/lib/src/views/chats/search_username_view.dart @@ -58,6 +58,7 @@ class _SearchUsernameView extends State { if (added > 0) { if (await SignalHelper.addNewContact(res.value.userdata)) { encryptAndSendMessage( + null, res.value.userdata.userId.toInt(), MessageJson( kind: MessageKind.contactRequest, @@ -207,6 +208,7 @@ class _ContactsListViewState extends State { await twonlyDatabase .deleteContactByUserId(contact.userId); encryptAndSendMessage( + null, contact.userId, MessageJson( kind: MessageKind.rejectRequest, @@ -223,6 +225,7 @@ class _ContactsListViewState extends State { final update = ContactsCompanion(accepted: Value(true)); await twonlyDatabase.updateContact(contact.userId, update); encryptAndSendMessage( + null, contact.userId, MessageJson( kind: MessageKind.acceptRequest, diff --git a/pubspec.lock b/pubspec.lock index 0560602..ab8e409 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -249,6 +249,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" + cryptography_flutter_plus: + dependency: "direct main" + description: + name: cryptography_flutter_plus + sha256: "35a8c270aae0abaac7125a6b6b33c2b3daa0ea90d85320aa7d588b6dd6c2edc9" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + cryptography_plus: + dependency: "direct main" + description: + name: cryptography_plus + sha256: "34db787df4f4740a39474b6fb0a610aa6dc13a5b5b68754b4787a79939ac0454" + url: "https://pub.dev" + source: hosted + version: "2.7.1" cv: dependency: "direct main" description: @@ -567,50 +583,50 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + sha256: f7eceb0bc6f4fd0441e29d43cab9ac2a1c5ffd7ea7b64075136b718c46954874 url: "https://pub.dev" source: hosted - version: "9.2.4" + version: "10.0.0-beta.4" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: f226f2a572bed96bc6542198ebaec227150786e34311d455a7e2d3d06d951845 + url: "https://pub.dev" + source: hosted + version: "0.1.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 + sha256: "9b4b73127e857cd3117d43a70fa3dddadb6e0b253be62e6a6ab85caa0742182c" url: "https://pub.dev" source: hosted - version: "1.2.2" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" - url: "https://pub.dev" - source: hosted - version: "3.1.3" + version: "2.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + sha256: "4c3f233e739545c6cb09286eeec1cc4744138372b985113acc904f7263bef517" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.0.0" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + sha256: ff32af20f70a8d0e59b2938fc92de35b54a74671041c814275afd80e27df9f21 url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -745,10 +761,10 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index bffdc92..bb3455b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,7 @@ dependencies: flutter_local_notifications: ^18.0.1 flutter_localizations: sdk: flutter - flutter_secure_storage: ^9.2.2 + flutter_secure_storage: ^10.0.0-beta.4 font_awesome_flutter: ^10.8.0 gal: ^2.3.1 google_fonts: ^6.2.1 @@ -50,6 +50,8 @@ dependencies: permission_handler: ^11.3.1 pie_menu: ^3.2.7 protobuf: ^2.1.0 + cryptography_plus: ^2.7.0 + cryptography_flutter_plus: ^2.3.2 provider: ^6.1.2 qr_flutter: ^4.1.0 reorderables: ^0.6.0