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 {
// 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.

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/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 {

View file

@ -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<DateTime>(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<Map<String, dynamic>> 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<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter};
await _update(userId, updates, notifyFlutter: false);
}

View file

@ -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<int>? 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<SendImage> 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<int> mediaToken,

View file

@ -181,8 +181,7 @@ Future<client.Response> 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) {

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/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<ChatListView> {
.where((c) => c.accepted)
.toList();
List<Contact> 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<ChatListView> {
)
],
),
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<ChatListView> {
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<UserListItem> {
int lastMessageInSeconds = 0;
MessageSendState state = MessageSendState.send;
bool isDownloading = false;
List<int> 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<int> token = [];
final content = widget.lastMessage!.messageContent;
if (widget.lastMessage.messageReceived && content is MediaMessageContent) {
token = content.downloadToken;
isDownloading = context
.watch<DownloadChangeProvider>()
.currentlyDownloading
.contains(token.toString());
if (widget.lastMessage!.messageReceived &&
content is MediaMessageContent) {
token = content.downloadToken;
isDownloading = context
.watch<DownloadChangeProvider>()
.currentlyDownloading
.contains(token.toString());
}
}
int flameCounter = context
.watch<MessagesChangeProvider>()
.flamesCounter[widget.user.userId.toInt()] ??
@ -186,39 +182,45 @@ class _UserListItem extends State<UserListItem> {
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;