mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:28:41 +00:00
parent
59bfec9667
commit
89174a9c63
6 changed files with 158 additions and 112 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue