This commit is contained in:
otsmr 2025-02-10 20:16:30 +01:00
parent 59bfec9667
commit 89174a9c63
6 changed files with 158 additions and 112 deletions

View file

@ -24,7 +24,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // 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 multiDexEnabled true
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.

View file

@ -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/contacts_change_provider.dart';
import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart';
import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/services/notification_service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'src/app.dart'; import 'src/app.dart';
void main() async { void main() async {

View file

@ -1,6 +1,5 @@
import 'package:cv/cv.dart'; import 'package:cv/cv.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:sqflite_sqlcipher/sqflite.dart'; import 'package:sqflite_sqlcipher/sqflite.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
@ -52,8 +51,6 @@ class DbContacts extends CvModelBase {
static const columnCreatedAt = "created_at"; static const columnCreatedAt = "created_at";
final createdAt = CvField<DateTime>(columnCreatedAt); final createdAt = CvField<DateTime>(columnCreatedAt);
static const nextFlameCounterInSeconds = kDebugMode ? 60 : 60 * 60 * 24;
static Future setupDatabaseTable(Database db) async { static Future setupDatabaseTable(Database db) async {
String createTableString = """ String createTableString = """
CREATE TABLE IF NOT EXISTS $tableName ( CREATE TABLE IF NOT EXISTS $tableName (
@ -100,9 +97,9 @@ class DbContacts extends CvModelBase {
return await _getAllUsers(); return await _getAllUsers();
} }
static Future checkAndUpdateFlames(int userId, {DateTime? timestamp}) async { static Future updateTotalMediaCounter(
timestamp ??= DateTime.now(); int userId,
) async {
List<Map<String, dynamic>> result = await dbProvider.db!.query( List<Map<String, dynamic>> result = await dbProvider.db!.query(
tableName, tableName,
columns: [columnTotalMediaCounter], columns: [columnTotalMediaCounter],
@ -112,7 +109,7 @@ class DbContacts extends CvModelBase {
if (result.isNotEmpty) { if (result.isNotEmpty) {
int totalMediaCounter = result.first.cast()[columnTotalMediaCounter]; int totalMediaCounter = result.first.cast()[columnTotalMediaCounter];
_updateFlameCounter(userId, totalMediaCounter + 1); _updateTotalMediaCounter(userId, totalMediaCounter + 1);
globalCallBackOnContactChange(); globalCallBackOnContactChange();
} }
} }
@ -205,7 +202,8 @@ class DbContacts extends CvModelBase {
await _update(userId, updates); await _update(userId, updates);
} }
static Future _updateFlameCounter(int userId, int totalMediaCounter) async { static Future _updateTotalMediaCounter(
int userId, int totalMediaCounter) async {
Map<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter}; Map<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter};
await _update(userId, updates, notifyFlutter: false); await _update(userId, updates, notifyFlutter: false);
} }

View file

@ -110,10 +110,6 @@ Future uploadMediaFile(
) async { ) async {
Box box = await getMediaStorage(); Box box = await getMediaStorage();
if ((await box.get("retransmit-$messageId-media") == null)) {
await box.put("retransmit-$messageId-media", encryptedMedia);
}
List<int>? uploadToken = await box.get("retransmit-$messageId-uploadtoken"); List<int>? uploadToken = await box.get("retransmit-$messageId-uploadtoken");
if (uploadToken == null) { if (uploadToken == null) {
Result res = await apiProvider.getUploadToken(); Result res = await apiProvider.getUploadToken();
@ -130,20 +126,34 @@ Future uploadMediaFile(
if (uploadToken == null) return; 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..."); int fragmentedTransportSize = 100000;
// TODO: fragmented upload...
if (!wasSend) { while (offset < encryptedMedia.length) {
Logger("api.dart").shout("error while uploading media"); int end = encryptedMedia.length;
return; 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"); Logger("api.dart").shout("DOING UPDATE");
box.delete("retransmit-$messageId-media"); box.delete("retransmit-$messageId-media");
box.delete("retransmit-$messageId-uploadtoken"); box.delete("retransmit-$messageId-uploadtoken");
await DbContacts.checkAndUpdateFlames(target.toInt());
await DbContacts.updateTotalMediaCounter(target.toInt());
// Ensures the retransmit of the message // Ensures the retransmit of the message
await encryptAndSendMessage( await encryptAndSendMessage(
@ -161,15 +171,40 @@ Future uploadMediaFile(
); );
} }
Future encryptAndUploadMediaFile( class SendImage {
Int64 target, final Int64 userId;
Uint8List imageBytes, final Uint8List imageBytes;
bool isRealTwonly, final bool isRealTwonly;
int maxShowTime, final int maxShowTime;
) async { DateTime? messageSendAt;
DateTime messageSendAt = DateTime.now(); int? messageId;
int? messageId = await DbMessages.insertMyMessage( Uint8List? encryptBytes;
target.toInt(),
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, MessageKind.image,
MediaMessageContent( MediaMessageContent(
downloadToken: [], downloadToken: [],
@ -177,19 +212,15 @@ Future encryptAndUploadMediaFile(
isRealTwonly: isRealTwonly, isRealTwonly: isRealTwonly,
isVideo: false, isVideo: false,
), ),
messageSendAt); messageSendAt!,
// isRealTwonly, );
if (messageId == null) return; // 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); Box box = await getMediaStorage();
if (encryptBytes == null) { await box.put("retransmit-$messageId-media", encryptBytes);
await DbMessages.deleteMessageById(messageId); // message is safe until now -> would be retransmitted if sending would fail..
Logger("api.dart").shout("Error encrypting media! Deleting media.");
return;
} }
await uploadMediaFile(messageId, target, encryptBytes, isRealTwonly,
maxShowTime, messageSendAt);
} }
Future sendImage( Future sendImage(
@ -198,22 +229,39 @@ Future sendImage(
bool isRealTwonly, bool isRealTwonly,
int maxShowTime, int maxShowTime,
) async { ) async {
// 1. set notifier provider
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes); Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
if (imageBytesCompressed == null) { if (imageBytesCompressed == null) {
Logger("api.dart").shout("Error compressing image!"); Logger("api.dart").shout("Error compressing image!");
return; return;
} }
if (imageBytesCompressed.length >= 10000000) {
Logger("api.dart").shout("Image to big aborting!");
return;
}
List<SendImage> tasks = [];
for (int i = 0; i < userIds.length; i++) { for (int i = 0; i < userIds.length; i++) {
encryptAndUploadMediaFile( tasks.add(
userIds[i], SendImage(
imageBytesCompressed, userId: userIds[i],
isRealTwonly, imageBytes: imageBytesCompressed,
maxShowTime, 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<int> mediaToken, Future tryDownloadMedia(int messageId, int fromUserId, List<int> mediaToken,

View file

@ -181,8 +181,7 @@ Future<client.Response> handleNewMessage(
if (message.kind == MessageKind.video || if (message.kind == MessageKind.video ||
message.kind == MessageKind.image) { message.kind == MessageKind.image) {
await DbContacts.checkAndUpdateFlames(fromUserId.toInt(), await DbContacts.updateTotalMediaCounter(fromUserId.toInt());
timestamp: message.timestamp);
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
final content = message.content; final content = message.content;
if (content is MediaMessageContent) { if (content is MediaMessageContent) {

View file

@ -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/providers/messages_change_provider.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/chats/chat_item_details_view.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/chats/media_viewer_view.dart';
import 'package:twonly/src/views/settings/settings_main_view.dart'; import 'package:twonly/src/views/settings/settings_main_view.dart';
import 'package:twonly/src/views/chats/search_username_view.dart'; import 'package:twonly/src/views/chats/search_username_view.dart';
@ -40,13 +39,13 @@ class _ChatListViewState extends State<ChatListView> {
.where((c) => c.accepted) .where((c) => c.accepted)
.toList(); .toList();
List<Contact> activeUsers = allUsers allUsers.sort((b, a) {
.where((x) => lastMessages.containsKey(x.userId.toInt())) DbMessage? msgA = lastMessages[a.userId.toInt()];
.toList(); DbMessage? msgB = lastMessages[a.userId.toInt()];
activeUsers.sort((b, a) { if (msgA == null) return 1;
return lastMessages[a.userId.toInt()]! if (msgB == null) return -1;
.sendAt
.compareTo(lastMessages[b.userId.toInt()]!.sendAt); return msgA.sendAt.compareTo(msgB.sendAt);
}); });
int maxTotalMediaCounter = 0; int maxTotalMediaCounter = 0;
@ -101,38 +100,32 @@ class _ChatListViewState extends State<ChatListView> {
) )
], ],
), ),
body: (activeUsers.isEmpty) body: (allUsers.isEmpty)
? Center( ? Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: OutlinedButton.icon( child: OutlinedButton.icon(
icon: Icon((allUsers.isEmpty) icon: Icon(Icons.person_add),
? Icons.person_add
: Icons.camera_alt),
onPressed: () { onPressed: () {
(allUsers.isEmpty) Navigator.push(
? Navigator.push( context,
context, MaterialPageRoute(
MaterialPageRoute( builder: (context) => SearchUsernameView(),
builder: (context) => SearchUsernameView(), ),
), );
)
: globalUpdateOfHomeViewPageIndex(0);
}, },
label: Text((allUsers.isEmpty) label: Text(context.lang.chatListViewSearchUserNameBtn)),
? context.lang.chatListViewSearchUserNameBtn
: context.lang.chatListViewSendFirstTwonly)),
), ),
) )
: ListView.builder( : ListView.builder(
restorationId: 'chat_list_view', restorationId: 'chat_list_view',
itemCount: activeUsers.length, itemCount: allUsers.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final user = activeUsers[index]; final user = allUsers[index];
return UserListItem( return UserListItem(
user: user, user: user,
maxTotalMediaCounter: maxTotalMediaCounter, maxTotalMediaCounter: maxTotalMediaCounter,
lastMessage: lastMessages[user.userId.toInt()]!, lastMessage: lastMessages[user.userId.toInt()],
); );
}, },
), ),
@ -142,7 +135,7 @@ class _ChatListViewState extends State<ChatListView> {
class UserListItem extends StatefulWidget { class UserListItem extends StatefulWidget {
final Contact user; final Contact user;
final DbMessage lastMessage; final DbMessage? lastMessage;
final int maxTotalMediaCounter; final int maxTotalMediaCounter;
const UserListItem( const UserListItem(
@ -157,26 +150,29 @@ class UserListItem extends StatefulWidget {
class _UserListItem extends State<UserListItem> { class _UserListItem extends State<UserListItem> {
int lastMessageInSeconds = 0; int lastMessageInSeconds = 0;
MessageSendState state = MessageSendState.send;
bool isDownloading = false;
List<int> token = [];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
int lastMessageInSeconds = if (widget.lastMessage != null) {
DateTime.now().difference(widget.lastMessage.sendAt).inSeconds; lastMessageInSeconds =
DateTime.now().difference(widget.lastMessage!.sendAt).inSeconds;
MessageSendState state = widget.lastMessage.getSendState(); state = widget.lastMessage!.getSendState();
bool isDownloading = false;
final content = widget.lastMessage.messageContent; final content = widget.lastMessage!.messageContent;
List<int> token = [];
if (widget.lastMessage.messageReceived && content is MediaMessageContent) { if (widget.lastMessage!.messageReceived &&
token = content.downloadToken; content is MediaMessageContent) {
isDownloading = context token = content.downloadToken;
.watch<DownloadChangeProvider>() isDownloading = context
.currentlyDownloading .watch<DownloadChangeProvider>()
.contains(token.toString()); .currentlyDownloading
.contains(token.toString());
}
} }
int flameCounter = context int flameCounter = context
.watch<MessagesChangeProvider>() .watch<MessagesChangeProvider>()
.flamesCounter[widget.user.userId.toInt()] ?? .flamesCounter[widget.user.userId.toInt()] ??
@ -186,39 +182,45 @@ class _UserListItem extends State<UserListItem> {
user: widget.user, user: widget.user,
child: ListTile( child: ListTile(
title: Text(widget.user.displayName), title: Text(widget.user.displayName),
subtitle: Row( subtitle: (widget.lastMessage == null)
children: [ ? Text("Tap to send your first image.")
MessageSendStateIcon(widget.lastMessage), : Row(
Text(""), children: [
const SizedBox(width: 5), MessageSendStateIcon(widget.lastMessage!),
Text( Text(""),
formatDuration(lastMessageInSeconds), const SizedBox(width: 5),
style: TextStyle(fontSize: 12), Text(
), formatDuration(lastMessageInSeconds),
if (flameCounter > 0) style: TextStyle(fontSize: 12),
FlameCounterWidget( ),
widget.user, if (flameCounter > 0)
flameCounter, FlameCounterWidget(
widget.maxTotalMediaCounter, widget.user,
prefix: true, flameCounter,
widget.maxTotalMediaCounter,
prefix: true,
),
],
), ),
],
),
leading: InitialsAvatar(displayName: widget.user.displayName), leading: InitialsAvatar(displayName: widget.user.displayName),
onTap: () { onTap: () {
if (widget.lastMessage == null) {
print("TODO: implement sending to one person!");
return;
}
if (isDownloading) return; if (isDownloading) return;
if (!widget.lastMessage.isDownloaded) { if (!widget.lastMessage!.isDownloaded) {
tryDownloadMedia(widget.lastMessage.messageId, tryDownloadMedia(widget.lastMessage!.messageId,
widget.lastMessage.otherUserId, token, widget.lastMessage!.otherUserId, token,
force: true); force: true);
return; return;
} }
if (state == MessageSendState.received && if (state == MessageSendState.received &&
widget.lastMessage.containsOtherMedia()) { widget.lastMessage!.containsOtherMedia()) {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return MediaViewerView(widget.user, widget.lastMessage); return MediaViewerView(widget.user, widget.lastMessage!);
}), }),
); );
return; return;