send multiple images with one key does work now

This commit is contained in:
otsmr 2025-03-16 15:44:07 +01:00
parent c008174070
commit 6d9df0328d
20 changed files with 689 additions and 529 deletions

View file

@ -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

View file

@ -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';

View file

@ -57,28 +57,9 @@ class MessageSendStateIcon extends StatefulWidget {
}
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
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<MessageSendStateIcon> {
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<MessageSendStateIcon> {
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);
}

View file

@ -34,6 +34,16 @@ class TwonlyDatabase extends _$TwonlyDatabase {
.watch();
}
Stream<List<Message>> 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<List<Message>> watchLastMessage(int contactId) {
return (select(messages)
..where((t) => t.contactId.equals(contactId))
@ -51,9 +61,12 @@ class TwonlyDatabase extends _$TwonlyDatabase {
Future<List<Message>> 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<Message> getMessageByMessageId(int messageId) {
return select(messages)..where((t) => t.messageId.equals(messageId));
}
// ------------
Future<int> insertContact(ContactsCompanion contact) async {

View file

@ -104,20 +104,38 @@ class MessageContent {
}
class MediaMessageContent extends MessageContent {
final List<int> downloadToken;
final int maxShowTime;
final bool isRealTwonly;
final bool isVideo;
final List<int>? downloadToken;
final List<int>? encryptionKey;
final List<int>? encryptionMac;
final List<int>? 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<int>.from(json['downloadToken']),
downloadToken: json['downloadToken'] == null
? null
: List<int>.from(json['downloadToken']),
encryptionKey: json['encryptionKey'] == null
? null
: List<int>.from(json['encryptionKey']),
encryptionMac: json['encryptionMac'] == null
? null
: List<int>.from(json['encryptionMac']),
encryptionNonce: json['encryptionNonce'] == null
? null
: List<int>.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,
};

View file

@ -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);

View file

@ -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 = {

View file

@ -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<Message> 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<Message> retransmit =
await twonlyDatabase.getAllMessagesForRetransmitting();
// List<Message> 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<Result> encryptAndSendMessage(int userId, MessageJson msg) async {
Future<Result> encryptAndSendMessage(
int? messageId, int userId, MessageJson msg) async {
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
if (bytes == null) {
@ -85,21 +62,19 @@ Future<Result> 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<int>? 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<int> 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<SendImage> 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<int> mediaToken,
{bool force = false}) async {
if (globalIsAppInBackground) return;
if (!force) {
// TODO: create option to enable download via mobile data
final List<ConnectivityResult> 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<Uint8List?> getDownloadedMedia(
List<int> 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<Box> 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));
}

View file

@ -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<Message> 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<int> userIds;
late HashMap<int, int> messageIds;
late Uint8List imageBytes;
late bool isRealTwonly;
late int maxShowTime;
late DateTime messageSendAt;
}
class PrepareState {
late List<int> sha2Hash;
late List<int> encryptionKey;
late List<int> encryptionMac;
late List<int> encryptedBytes;
late List<int> encryptionNonce;
}
class UploadState {
late List<int> uploadToken;
late List<List<int>> downloadTokens;
}
class ImageUploader {
static Future<PrepareState?> 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?> 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<int>? 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<int> 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<Uint8List?> getDownloadedMedia(
Message message, List<int> 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;
}

View file

@ -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<client.Response> 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<client.Response> 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<client.Response> 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<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
}
encryptAndSendMessage(
message.messageId!,
fromUserId,
MessageJson(
kind: MessageKind.ack,
@ -220,8 +258,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (!globalIsAppInBackground) {
final content = message.content;
if (content is MediaMessageContent) {
List<int> downloadToken = content.downloadToken;
tryDownloadMedia(messageId, fromUserId, downloadToken);
tryDownloadMedia(
messageId,
fromUserId,
content,
);
}
}
}

View file

@ -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<Result> getUploadToken() async {
var get = ApplicationData_GetUploadToken();
Future<Result> 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<Result> uploadData(
List<int> uploadToken, Uint8List data, int offset) async {
Future<Result> uploadData(List<int> uploadToken, Uint8List data, int offset,
List<int>? 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);

View file

@ -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<Box> 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));
}

View file

@ -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';

View file

@ -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<ShareImageView> {
//_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<ShareImageView> {
setState(() {
sendingImage = true;
});
await sendImage(
sendImage(
_selectedUserIds.toList(),
imageBytes!,
widget.isRealTwonly,

View file

@ -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<int> 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);
}
}

View file

@ -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<UserListItem> {
int lastMessageInSeconds = 0;
MessageSendState state = MessageSendState.send;
List<int> token = [];
Message? currentMessage;
Timer? updateTime;
@ -209,7 +209,14 @@ class _UserListItem extends State<UserListItem> {
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<UserListItem> {
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,

View file

@ -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<MediaViewerView> {
void initState() {
super.initState();
asyncLoadNextMedia();
loadCurrentMediaFile();
asyncLoadNextMedia(true);
}
Future asyncLoadNextMedia() async {
Future asyncLoadNextMedia(bool firstRun) async {
Stream<List<Message>> 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<MediaViewerView> {
}
}
setState(() {});
if (firstRun) {
loadCurrentMediaFile();
firstRun = false;
}
});
}
@ -117,42 +121,38 @@ class _MediaViewerViewState extends State<MediaViewerView> {
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() {

View file

@ -58,6 +58,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
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<ContactsListView> {
await twonlyDatabase
.deleteContactByUserId(contact.userId);
encryptAndSendMessage(
null,
contact.userId,
MessageJson(
kind: MessageKind.rejectRequest,
@ -223,6 +225,7 @@ class _ContactsListViewState extends State<ContactsListView> {
final update = ContactsCompanion(accepted: Value(true));
await twonlyDatabase.updateContact(contact.userId, update);
encryptAndSendMessage(
null,
contact.userId,
MessageJson(
kind: MessageKind.acceptRequest,

View file

@ -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:

View file

@ -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