mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:08:40 +00:00
send multiple images with one key does work now
This commit is contained in:
parent
c008174070
commit
6d9df0328d
20 changed files with 689 additions and 529 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
319
lib/src/providers/api/media.dart
Normal file
319
lib/src/providers/api/media.dart
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
30
lib/src/providers/hive.dart
Normal file
30
lib/src/providers/hive.dart
Normal 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));
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
56
pubspec.lock
56
pubspec.lock
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue