diff --git a/android/app/build.gradle b/android/app/build.gradle index 0adb1be..1594b64 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "eu.twonly" + applicationId = "eu.twonly.testing" multiDexEnabled true // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. diff --git a/lib/main.dart b/lib/main.dart index 693299e..1467f7e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/services/notification_service.dart'; -import 'package:twonly/src/utils/misc.dart'; import 'src/app.dart'; void main() async { diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index dd0c131..80ce2ec 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -1,6 +1,5 @@ import 'package:cv/cv.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:twonly/globals.dart'; @@ -52,8 +51,6 @@ class DbContacts extends CvModelBase { static const columnCreatedAt = "created_at"; final createdAt = CvField(columnCreatedAt); - static const nextFlameCounterInSeconds = kDebugMode ? 60 : 60 * 60 * 24; - static Future setupDatabaseTable(Database db) async { String createTableString = """ CREATE TABLE IF NOT EXISTS $tableName ( @@ -100,9 +97,9 @@ class DbContacts extends CvModelBase { return await _getAllUsers(); } - static Future checkAndUpdateFlames(int userId, {DateTime? timestamp}) async { - timestamp ??= DateTime.now(); - + static Future updateTotalMediaCounter( + int userId, + ) async { List> result = await dbProvider.db!.query( tableName, columns: [columnTotalMediaCounter], @@ -112,7 +109,7 @@ class DbContacts extends CvModelBase { if (result.isNotEmpty) { int totalMediaCounter = result.first.cast()[columnTotalMediaCounter]; - _updateFlameCounter(userId, totalMediaCounter + 1); + _updateTotalMediaCounter(userId, totalMediaCounter + 1); globalCallBackOnContactChange(); } } @@ -205,7 +202,8 @@ class DbContacts extends CvModelBase { await _update(userId, updates); } - static Future _updateFlameCounter(int userId, int totalMediaCounter) async { + static Future _updateTotalMediaCounter( + int userId, int totalMediaCounter) async { Map updates = {columnTotalMediaCounter: totalMediaCounter}; await _update(userId, updates, notifyFlutter: false); } diff --git a/lib/src/providers/api/api.dart b/lib/src/providers/api/api.dart index b216585..2f65d9f 100644 --- a/lib/src/providers/api/api.dart +++ b/lib/src/providers/api/api.dart @@ -110,10 +110,6 @@ Future uploadMediaFile( ) async { Box box = await getMediaStorage(); - if ((await box.get("retransmit-$messageId-media") == null)) { - await box.put("retransmit-$messageId-media", encryptedMedia); - } - List? uploadToken = await box.get("retransmit-$messageId-uploadtoken"); if (uploadToken == null) { Result res = await apiProvider.getUploadToken(); @@ -130,20 +126,34 @@ Future uploadMediaFile( if (uploadToken == null) return; - bool wasSend = await apiProvider.uploadData(uploadToken, encryptedMedia, 0); + int offset = await box.get("retransmit-$messageId-offset") ?? 0; - Logger("api.dart").shout("UPDATE..."); - // TODO: fragmented upload... - if (!wasSend) { - Logger("api.dart").shout("error while uploading media"); - return; + int fragmentedTransportSize = 100000; + + while (offset < encryptedMedia.length) { + int end = encryptedMedia.length; + if (offset + fragmentedTransportSize < encryptedMedia.length) { + end = offset + fragmentedTransportSize; + } + bool wasSend = await apiProvider.uploadData( + uploadToken, encryptedMedia.sublist(offset, end), offset); + + if (!wasSend) { + Logger("api.dart").shout("error while uploading media"); + return; + } + + await box.put("retransmit-$messageId-offset", offset); + + offset = end; } Logger("api.dart").shout("DOING UPDATE"); box.delete("retransmit-$messageId-media"); box.delete("retransmit-$messageId-uploadtoken"); - await DbContacts.checkAndUpdateFlames(target.toInt()); + + await DbContacts.updateTotalMediaCounter(target.toInt()); // Ensures the retransmit of the message await encryptAndSendMessage( @@ -161,15 +171,40 @@ Future uploadMediaFile( ); } -Future encryptAndUploadMediaFile( - Int64 target, - Uint8List imageBytes, - bool isRealTwonly, - int maxShowTime, -) async { - DateTime messageSendAt = DateTime.now(); - int? messageId = await DbMessages.insertMyMessage( - target.toInt(), +class SendImage { + final Int64 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(); + messageId = await DbMessages.insertMyMessage( + userId.toInt(), MessageKind.image, MediaMessageContent( downloadToken: [], @@ -177,19 +212,15 @@ Future encryptAndUploadMediaFile( isRealTwonly: isRealTwonly, isVideo: false, ), - messageSendAt); - // isRealTwonly, - if (messageId == null) return; + messageSendAt!, + ); + // should only happen when there is no space left on the smartphone -> abort message + if (messageId == null) return; - Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target); - if (encryptBytes == null) { - await DbMessages.deleteMessageById(messageId); - Logger("api.dart").shout("Error encrypting media! Deleting media."); - return; + Box box = await getMediaStorage(); + await box.put("retransmit-$messageId-media", encryptBytes); + // message is safe until now -> would be retransmitted if sending would fail.. } - - await uploadMediaFile(messageId, target, encryptBytes, isRealTwonly, - maxShowTime, messageSendAt); } Future sendImage( @@ -198,22 +229,39 @@ Future sendImage( bool isRealTwonly, int maxShowTime, ) async { - // 1. set notifier provider - 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++) { - encryptAndUploadMediaFile( - userIds[i], - imageBytesCompressed, - isRealTwonly, - maxShowTime, + tasks.add( + SendImage( + userId: userIds[i], + imageBytes: imageBytesCompressed, + isRealTwonly: isRealTwonly, + maxShowTime: maxShowTime, + ), ); } + + // first step encrypt and store the encrypted image + for (SendImage task in tasks) { + await task.encryptAndStore(); + } + + // after the images are safely stored try do upload them one by one + for (SendImage task in tasks) { + await task.upload(); + } } Future tryDownloadMedia(int messageId, int fromUserId, List mediaToken, diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index ea252b3..3f03288 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -181,8 +181,7 @@ Future handleNewMessage( if (message.kind == MessageKind.video || message.kind == MessageKind.image) { - await DbContacts.checkAndUpdateFlames(fromUserId.toInt(), - timestamp: message.timestamp); + await DbContacts.updateTotalMediaCounter(fromUserId.toInt()); if (!globalIsAppInBackground) { final content = message.content; if (content is MediaMessageContent) { diff --git a/lib/src/views/chats/chat_list_view.dart b/lib/src/views/chats/chat_list_view.dart index 34163c9..8f1d1e3 100644 --- a/lib/src/views/chats/chat_list_view.dart +++ b/lib/src/views/chats/chat_list_view.dart @@ -14,7 +14,6 @@ import 'package:twonly/src/providers/download_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/chats/chat_item_details_view.dart'; -import 'package:twonly/src/views/home_view.dart'; import 'package:twonly/src/views/chats/media_viewer_view.dart'; import 'package:twonly/src/views/settings/settings_main_view.dart'; import 'package:twonly/src/views/chats/search_username_view.dart'; @@ -40,13 +39,13 @@ class _ChatListViewState extends State { .where((c) => c.accepted) .toList(); - List activeUsers = allUsers - .where((x) => lastMessages.containsKey(x.userId.toInt())) - .toList(); - activeUsers.sort((b, a) { - return lastMessages[a.userId.toInt()]! - .sendAt - .compareTo(lastMessages[b.userId.toInt()]!.sendAt); + allUsers.sort((b, a) { + DbMessage? msgA = lastMessages[a.userId.toInt()]; + DbMessage? msgB = lastMessages[a.userId.toInt()]; + if (msgA == null) return 1; + if (msgB == null) return -1; + + return msgA.sendAt.compareTo(msgB.sendAt); }); int maxTotalMediaCounter = 0; @@ -101,38 +100,32 @@ class _ChatListViewState extends State { ) ], ), - body: (activeUsers.isEmpty) + body: (allUsers.isEmpty) ? Center( child: Padding( padding: const EdgeInsets.all(10), child: OutlinedButton.icon( - icon: Icon((allUsers.isEmpty) - ? Icons.person_add - : Icons.camera_alt), + icon: Icon(Icons.person_add), onPressed: () { - (allUsers.isEmpty) - ? Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SearchUsernameView(), - ), - ) - : globalUpdateOfHomeViewPageIndex(0); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchUsernameView(), + ), + ); }, - label: Text((allUsers.isEmpty) - ? context.lang.chatListViewSearchUserNameBtn - : context.lang.chatListViewSendFirstTwonly)), + label: Text(context.lang.chatListViewSearchUserNameBtn)), ), ) : ListView.builder( restorationId: 'chat_list_view', - itemCount: activeUsers.length, + itemCount: allUsers.length, itemBuilder: (BuildContext context, int index) { - final user = activeUsers[index]; + final user = allUsers[index]; return UserListItem( user: user, maxTotalMediaCounter: maxTotalMediaCounter, - lastMessage: lastMessages[user.userId.toInt()]!, + lastMessage: lastMessages[user.userId.toInt()], ); }, ), @@ -142,7 +135,7 @@ class _ChatListViewState extends State { class UserListItem extends StatefulWidget { final Contact user; - final DbMessage lastMessage; + final DbMessage? lastMessage; final int maxTotalMediaCounter; const UserListItem( @@ -157,26 +150,29 @@ class UserListItem extends StatefulWidget { class _UserListItem extends State { int lastMessageInSeconds = 0; + MessageSendState state = MessageSendState.send; + bool isDownloading = false; + List token = []; @override Widget build(BuildContext context) { - int lastMessageInSeconds = - DateTime.now().difference(widget.lastMessage.sendAt).inSeconds; + if (widget.lastMessage != null) { + lastMessageInSeconds = + DateTime.now().difference(widget.lastMessage!.sendAt).inSeconds; - MessageSendState state = widget.lastMessage.getSendState(); - bool isDownloading = false; + state = widget.lastMessage!.getSendState(); - final content = widget.lastMessage.messageContent; - List token = []; + final content = widget.lastMessage!.messageContent; - if (widget.lastMessage.messageReceived && content is MediaMessageContent) { - token = content.downloadToken; - isDownloading = context - .watch() - .currentlyDownloading - .contains(token.toString()); + if (widget.lastMessage!.messageReceived && + content is MediaMessageContent) { + token = content.downloadToken; + isDownloading = context + .watch() + .currentlyDownloading + .contains(token.toString()); + } } - int flameCounter = context .watch() .flamesCounter[widget.user.userId.toInt()] ?? @@ -186,39 +182,45 @@ class _UserListItem extends State { user: widget.user, child: ListTile( title: Text(widget.user.displayName), - subtitle: Row( - children: [ - MessageSendStateIcon(widget.lastMessage), - Text("•"), - const SizedBox(width: 5), - Text( - formatDuration(lastMessageInSeconds), - style: TextStyle(fontSize: 12), - ), - if (flameCounter > 0) - FlameCounterWidget( - widget.user, - flameCounter, - widget.maxTotalMediaCounter, - prefix: true, + subtitle: (widget.lastMessage == null) + ? Text("Tap to send your first image.") + : Row( + children: [ + MessageSendStateIcon(widget.lastMessage!), + Text("•"), + const SizedBox(width: 5), + Text( + formatDuration(lastMessageInSeconds), + style: TextStyle(fontSize: 12), + ), + if (flameCounter > 0) + FlameCounterWidget( + widget.user, + flameCounter, + widget.maxTotalMediaCounter, + prefix: true, + ), + ], ), - ], - ), leading: InitialsAvatar(displayName: widget.user.displayName), onTap: () { + if (widget.lastMessage == null) { + print("TODO: implement sending to one person!"); + return; + } if (isDownloading) return; - if (!widget.lastMessage.isDownloaded) { - tryDownloadMedia(widget.lastMessage.messageId, - widget.lastMessage.otherUserId, token, + if (!widget.lastMessage!.isDownloaded) { + tryDownloadMedia(widget.lastMessage!.messageId, + widget.lastMessage!.otherUserId, token, force: true); return; } if (state == MessageSendState.received && - widget.lastMessage.containsOtherMedia()) { + widget.lastMessage!.containsOtherMedia()) { Navigator.push( context, MaterialPageRoute(builder: (context) { - return MediaViewerView(widget.user, widget.lastMessage); + return MediaViewerView(widget.user, widget.lastMessage!); }), ); return;