mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 10:38:41 +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
|
multiDexEnabled true
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/database/database.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:twonly/src/providers/api_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:twonly/src/providers/db_provider.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/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/providers/settings_change_provider.dart';
|
import 'package:twonly/src/providers/settings_change_provider.dart';
|
||||||
import 'package:twonly/src/services/fcm_service.dart';
|
import 'package:twonly/src/services/fcm_service.dart';
|
||||||
|
|
|
||||||
|
|
@ -57,28 +57,9 @@ class MessageSendStateIcon extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
Message? videoMsg;
|
|
||||||
Message? textMsg;
|
|
||||||
Message? imageMsg;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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) {
|
Widget getLoaderIcon(color) {
|
||||||
|
|
@ -128,6 +109,15 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
case MessageSendState.received:
|
case MessageSendState.received:
|
||||||
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
icon = Icon(Icons.square_rounded, size: 14, color: color);
|
||||||
text = context.lang.messageSendState_Received;
|
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;
|
break;
|
||||||
case MessageSendState.send:
|
case MessageSendState.send:
|
||||||
icon =
|
icon =
|
||||||
|
|
@ -135,21 +125,14 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
text = context.lang.messageSendState_Send;
|
text = context.lang.messageSendState_Send;
|
||||||
break;
|
break;
|
||||||
case MessageSendState.sending:
|
case MessageSendState.sending:
|
||||||
case MessageSendState.receiving:
|
|
||||||
icon = getLoaderIcon(color);
|
icon = getLoaderIcon(color);
|
||||||
text = context.lang.messageSendState_Sending;
|
text = context.lang.messageSendState_Sending;
|
||||||
|
case MessageSendState.receiving:
|
||||||
|
icon = getLoaderIcon(color);
|
||||||
|
text = context.lang.messageSendState_Received;
|
||||||
break;
|
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);
|
icons.add(icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,16 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
||||||
.watch();
|
.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) {
|
Stream<List<Message>> watchLastMessage(int contactId) {
|
||||||
return (select(messages)
|
return (select(messages)
|
||||||
..where((t) => t.contactId.equals(contactId))
|
..where((t) => t.contactId.equals(contactId))
|
||||||
|
|
@ -51,9 +61,12 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
||||||
|
|
||||||
Future<List<Message>> getAllMessagesPendingDownloading() {
|
Future<List<Message>> getAllMessagesPendingDownloading() {
|
||||||
return (select(messages)
|
return (select(messages)
|
||||||
..where((t) =>
|
..where(
|
||||||
|
(t) =>
|
||||||
t.downloadState.equals(DownloadState.downloaded.index).not() &
|
t.downloadState.equals(DownloadState.downloaded.index).not() &
|
||||||
t.kind.equals(MessageKind.media.name)))
|
t.messageOtherId.isNotNull() &
|
||||||
|
t.kind.equals(MessageKind.media.name),
|
||||||
|
))
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,6 +121,10 @@ class TwonlyDatabase extends _$TwonlyDatabase {
|
||||||
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
|
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 {
|
Future<int> insertContact(ContactsCompanion contact) async {
|
||||||
|
|
|
||||||
|
|
@ -104,20 +104,38 @@ class MessageContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
class MediaMessageContent extends MessageContent {
|
class MediaMessageContent extends MessageContent {
|
||||||
final List<int> downloadToken;
|
|
||||||
final int maxShowTime;
|
final int maxShowTime;
|
||||||
final bool isRealTwonly;
|
final bool isRealTwonly;
|
||||||
final bool isVideo;
|
final bool isVideo;
|
||||||
|
final List<int>? downloadToken;
|
||||||
|
final List<int>? encryptionKey;
|
||||||
|
final List<int>? encryptionMac;
|
||||||
|
final List<int>? encryptionNonce;
|
||||||
|
|
||||||
MediaMessageContent({
|
MediaMessageContent({
|
||||||
required this.downloadToken,
|
|
||||||
required this.maxShowTime,
|
required this.maxShowTime,
|
||||||
required this.isRealTwonly,
|
required this.isRealTwonly,
|
||||||
required this.isVideo,
|
required this.isVideo,
|
||||||
|
this.downloadToken,
|
||||||
|
this.encryptionKey,
|
||||||
|
this.encryptionMac,
|
||||||
|
this.encryptionNonce,
|
||||||
});
|
});
|
||||||
|
|
||||||
static MediaMessageContent fromJson(Map json) {
|
static MediaMessageContent fromJson(Map json) {
|
||||||
return MediaMessageContent(
|
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'],
|
maxShowTime: json['maxShowTime'],
|
||||||
isRealTwonly: json['isRealTwonly'],
|
isRealTwonly: json['isRealTwonly'],
|
||||||
isVideo: json['isVideo'] ?? false,
|
isVideo: json['isVideo'] ?? false,
|
||||||
|
|
@ -128,6 +146,9 @@ class MediaMessageContent extends MessageContent {
|
||||||
Map toJson() {
|
Map toJson() {
|
||||||
return {
|
return {
|
||||||
'downloadToken': downloadToken,
|
'downloadToken': downloadToken,
|
||||||
|
'encryptionKey': encryptionKey,
|
||||||
|
'encryptionMac': encryptionMac,
|
||||||
|
'encryptionNonce': encryptionNonce,
|
||||||
'isRealTwonly': isRealTwonly,
|
'isRealTwonly': isRealTwonly,
|
||||||
'maxShowTime': maxShowTime,
|
'maxShowTime': maxShowTime,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -294,14 +294,14 @@ class NewMessage extends $pb.GeneratedMessage {
|
||||||
|
|
||||||
class DownloadData extends $pb.GeneratedMessage {
|
class DownloadData extends $pb.GeneratedMessage {
|
||||||
factory DownloadData({
|
factory DownloadData({
|
||||||
$core.List<$core.int>? uploadToken,
|
$core.List<$core.int>? downloadToken,
|
||||||
$core.int? offset,
|
$core.int? offset,
|
||||||
$core.List<$core.int>? data,
|
$core.List<$core.int>? data,
|
||||||
$core.bool? fin,
|
$core.bool? fin,
|
||||||
}) {
|
}) {
|
||||||
final $result = create();
|
final $result = create();
|
||||||
if (uploadToken != null) {
|
if (downloadToken != null) {
|
||||||
$result.uploadToken = uploadToken;
|
$result.downloadToken = downloadToken;
|
||||||
}
|
}
|
||||||
if (offset != null) {
|
if (offset != null) {
|
||||||
$result.offset = offset;
|
$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);
|
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)
|
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.int>(2, _omitFieldNames ? '' : 'offset', $pb.PbFieldType.OU3)
|
||||||
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
|
..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
|
||||||
..aOB(4, _omitFieldNames ? '' : 'fin')
|
..aOB(4, _omitFieldNames ? '' : 'fin')
|
||||||
|
|
@ -348,13 +348,13 @@ class DownloadData extends $pb.GeneratedMessage {
|
||||||
static DownloadData? _defaultInstance;
|
static DownloadData? _defaultInstance;
|
||||||
|
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
$core.List<$core.int> get uploadToken => $_getN(0);
|
$core.List<$core.int> get downloadToken => $_getN(0);
|
||||||
@$pb.TagNumber(1)
|
@$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)
|
@$pb.TagNumber(1)
|
||||||
$core.bool hasUploadToken() => $_has(0);
|
$core.bool hasDownloadToken() => $_has(0);
|
||||||
@$pb.TagNumber(1)
|
@$pb.TagNumber(1)
|
||||||
void clearUploadToken() => clearField(1);
|
void clearDownloadToken() => clearField(1);
|
||||||
|
|
||||||
@$pb.TagNumber(2)
|
@$pb.TagNumber(2)
|
||||||
$core.int get offset => $_getIZ(1);
|
$core.int get offset => $_getIZ(1);
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ final $typed_data.Uint8List newMessageDescriptor = $convert.base64Decode(
|
||||||
const DownloadData$json = {
|
const DownloadData$json = {
|
||||||
'1': 'DownloadData',
|
'1': 'DownloadData',
|
||||||
'2': [
|
'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': 'offset', '3': 2, '4': 1, '5': 13, '10': 'offset'},
|
||||||
{'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'},
|
{'1': 'data', '3': 3, '4': 1, '5': 12, '10': 'data'},
|
||||||
{'1': 'fin', '3': 4, '4': 1, '5': 8, '10': 'fin'},
|
{'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`.
|
/// Descriptor for `DownloadData`. Decode as a `google.protobuf.DescriptorProto`.
|
||||||
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
|
final $typed_data.Uint8List downloadDataDescriptor = $convert.base64Decode(
|
||||||
'CgxEb3dubG9hZERhdGESIQoMdXBsb2FkX3Rva2VuGAEgASgMUgt1cGxvYWRUb2tlbhIWCgZvZm'
|
'CgxEb3dubG9hZERhdGESJQoOZG93bmxvYWRfdG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4SFg'
|
||||||
'ZzZXQYAiABKA1SBm9mZnNldBISCgRkYXRhGAMgASgMUgRkYXRhEhAKA2ZpbhgEIAEoCFIDZmlu');
|
'oGb2Zmc2V0GAIgASgNUgZvZmZzZXQSEgoEZGF0YRgDIAEoDFIEZGF0YRIQCgNmaW4YBCABKAhS'
|
||||||
|
'A2Zpbg==');
|
||||||
|
|
||||||
@$core.Deprecated('Use responseDescriptor instead')
|
@$core.Deprecated('Use responseDescriptor instead')
|
||||||
const Response$json = {
|
const Response$json = {
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,59 @@
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
import 'package:twonly/src/app.dart';
|
|
||||||
import 'package:twonly/src/database/database.dart';
|
import 'package:twonly/src/database/database.dart';
|
||||||
import 'package:twonly/src/database/messages_db.dart';
|
import 'package:twonly/src/database/messages_db.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/proto/api/error.pb.dart';
|
import 'package:twonly/src/proto/api/error.pb.dart';
|
||||||
import 'package:twonly/src/providers/api/api_utils.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
|
// ignore: library_prefixes
|
||||||
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
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 {
|
Future tryTransmitMessages() async {
|
||||||
List<Message> retransmit =
|
// List<Message> retransmit =
|
||||||
await twonlyDatabase.getAllMessagesForRetransmitting();
|
// 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();
|
// Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
|
||||||
for (int i = 0; i < retransmit.length; i++) {
|
// if (bytes != null) {
|
||||||
int msgId = retransmit[i].messageId;
|
// Result resp = await apiProvider.sendTextMessage(
|
||||||
|
// retransmit[i].contactId,
|
||||||
|
// bytes,
|
||||||
|
// );
|
||||||
|
|
||||||
Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
|
// if (resp.isSuccess) {
|
||||||
if (bytes != null) {
|
// await twonlyDatabase.updateMessageByMessageId(
|
||||||
Result resp = await apiProvider.sendTextMessage(
|
// msgId, MessagesCompanion(acknowledgeByServer: Value(true)));
|
||||||
retransmit[i].contactId,
|
|
||||||
bytes,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resp.isSuccess) {
|
// box.delete("retransmit-$msgId-textmessage");
|
||||||
await twonlyDatabase.updateMessageByMessageId(
|
// } else {
|
||||||
msgId,
|
// // in case of error do nothing. As the message is not removed the app will try again when relaunched
|
||||||
MessagesCompanion(acknowledgeByServer: Value(true))
|
// }
|
||||||
);
|
// }
|
||||||
|
|
||||||
box.delete("retransmit-$msgId-textmessage");
|
// Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media");
|
||||||
} else {
|
// if (encryptedMedia != null) {
|
||||||
// in case of error do nothing. As the message is not removed the app will try again when relaunched
|
// 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
|
// 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);
|
Uint8List? bytes = await SignalHelper.encryptMessage(msg, userId);
|
||||||
|
|
||||||
if (bytes == null) {
|
if (bytes == null) {
|
||||||
|
|
@ -85,21 +62,19 @@ Future<Result> encryptAndSendMessage(int userId, MessageJson msg) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
Box box = await getMediaStorage();
|
Box box = await getMediaStorage();
|
||||||
if (msg.messageId != null) {
|
if (messageId != null) {
|
||||||
box.put("retransmit-${msg.messageId}-textmessage", bytes);
|
box.put("retransmit-$messageId-textmessage", bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
Result resp = await apiProvider.sendTextMessage(userId, bytes);
|
||||||
|
|
||||||
if (resp.isSuccess) {
|
if (resp.isSuccess) {
|
||||||
if (msg.messageId != null) {
|
if (messageId != null) {
|
||||||
|
|
||||||
|
|
||||||
await twonlyDatabase.updateMessageByMessageId(
|
await twonlyDatabase.updateMessageByMessageId(
|
||||||
msg.messageId!,
|
messageId,
|
||||||
MessagesCompanion(acknowledgeByServer: Value(true))
|
MessagesCompanion(acknowledgeByServer: Value(true)),
|
||||||
);
|
);
|
||||||
box.delete("retransmit-${msg.messageId}-textmessage");
|
box.delete("retransmit-$messageId-textmessage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,14 +86,14 @@ Future sendTextMessage(int target, String message) async {
|
||||||
|
|
||||||
DateTime messageSendAt = DateTime.now();
|
DateTime messageSendAt = DateTime.now();
|
||||||
|
|
||||||
int? messageId = await twonlyDatabase.insertMessage(MessagesCompanion(
|
int? messageId = await twonlyDatabase.insertMessage(
|
||||||
|
MessagesCompanion(
|
||||||
contactId: Value(target),
|
contactId: Value(target),
|
||||||
kind: Value(MessageKind.textMessage),
|
kind: Value(MessageKind.textMessage),
|
||||||
sendAt: Value(messageSendAt),
|
sendAt: Value(messageSendAt),
|
||||||
downloadState: Value(DownloadState.downloaded),
|
downloadState: Value(DownloadState.downloaded),
|
||||||
contentJson: Value(jsonEncode(content.toJson()))
|
contentJson: Value(jsonEncode(content.toJson()))),
|
||||||
),);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (messageId == null) return;
|
if (messageId == null) return;
|
||||||
|
|
||||||
|
|
@ -129,226 +104,15 @@ Future sendTextMessage(int target, String message) async {
|
||||||
timestamp: messageSendAt,
|
timestamp: messageSendAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
encryptAndSendMessage(target, msg);
|
encryptAndSendMessage(messageId, target, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this will send the media file and ensures retransmission when errors occur
|
Future notifyContactAboutOpeningMessage(
|
||||||
Future uploadMediaFile(
|
int fromUserId, int messageOtherId) async {
|
||||||
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 {
|
|
||||||
//await DbMessages.userOpenedOtherMessage(fromUserId, messageOtherId);
|
//await DbMessages.userOpenedOtherMessage(fromUserId, messageOtherId);
|
||||||
|
|
||||||
encryptAndSendMessage(
|
encryptAndSendMessage(
|
||||||
|
null,
|
||||||
fromUserId,
|
fromUserId,
|
||||||
MessageJson(
|
MessageJson(
|
||||||
kind: MessageKind.opened,
|
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:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.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/proto/api/server_to_client.pbserver.dart';
|
||||||
import 'package:twonly/src/providers/api/api.dart';
|
import 'package:twonly/src/providers/api/api.dart';
|
||||||
import 'package:twonly/src/providers/api/api_utils.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';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
// ignore: library_prefixes
|
// ignore: library_prefixes
|
||||||
import 'package:twonly/src/utils/signal.dart' as SignalHelper;
|
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
|
// download should only be done when the app is open
|
||||||
return client.Response()..error = ErrorCode.InternalError;
|
return client.Response()..error = ErrorCode.InternalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger("server_messages")
|
Logger("server_messages")
|
||||||
.info("downloading: ${data.uploadToken} ${data.fin}");
|
.info("downloading: ${data.downloadToken} ${data.fin}");
|
||||||
|
|
||||||
final box = await getMediaStorage();
|
final box = await getMediaStorage();
|
||||||
|
|
||||||
String boxId = data.uploadToken.toString();
|
String boxId = data.downloadToken.toString();
|
||||||
int? messageId = box.get("${data.uploadToken}_messageId");
|
|
||||||
if (data.fin && data.data.isEmpty) {
|
|
||||||
// media file was deleted by the server. remove the media from device
|
|
||||||
|
|
||||||
if (messageId != null) {
|
int? messageId = box.get("${data.downloadToken}_messageId");
|
||||||
await twonlyDatabase.deleteMessageById(messageId);
|
|
||||||
box.delete(boxId);
|
if (messageId == null) {
|
||||||
box.delete("${data.uploadToken}_fromUserId");
|
Logger("server_messages")
|
||||||
box.delete("${data.uploadToken}_downloaded");
|
.info("download data received, but unknown messageID");
|
||||||
var ok = client.Response_Ok()..none = true;
|
// answers with ok, so the server will delete the message
|
||||||
return client.Response()..ok = ok;
|
|
||||||
} else {
|
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
return client.Response()..ok = ok;
|
return client.Response()..ok = ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.fin && data.data.isEmpty) {
|
||||||
|
// media file was deleted by the server. remove the media from device
|
||||||
|
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);
|
Uint8List? buffered = box.get(boxId);
|
||||||
|
|
@ -93,33 +101,59 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
|
||||||
downloadedBytes = Uint8List.fromList(data.data);
|
downloadedBytes = Uint8List.fromList(data.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.fin) {
|
if (!data.fin) {
|
||||||
SignalHelper.getSignalStore();
|
// download not finished, so waiting for more data...
|
||||||
int? fromUserId = box.get("${data.uploadToken}_fromUserId");
|
box.put(boxId, downloadedBytes);
|
||||||
if (fromUserId != null) {
|
var ok = client.Response_Ok()..none = true;
|
||||||
Uint8List? rawBytes =
|
return client.Response()..ok = ok;
|
||||||
await SignalHelper.decryptBytes(downloadedBytes, fromUserId);
|
}
|
||||||
|
|
||||||
if (rawBytes != null) {
|
// Uint8List? rawBytes =
|
||||||
box.put("${data.uploadToken}_downloaded", rawBytes);
|
// await SignalHelper.decryptBytes(downloadedBytes, fromUserId);
|
||||||
} else {
|
|
||||||
Logger("server_messages")
|
Message? msg =
|
||||||
.shout("error decrypting the message: ${data.uploadToken}");
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
final update =
|
|
||||||
MessagesCompanion(downloadState: Value(DownloadState.downloaded));
|
|
||||||
await twonlyDatabase.updateMessageByOtherUser(
|
await twonlyDatabase.updateMessageByOtherUser(
|
||||||
fromUserId,
|
msg.contactId,
|
||||||
messageId!,
|
messageId,
|
||||||
update,
|
MessagesCompanion(downloadState: Value(DownloadState.downloaded)),
|
||||||
);
|
);
|
||||||
|
|
||||||
box.delete(boxId);
|
box.delete(boxId);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
box.put(boxId, downloadedBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
var ok = client.Response_Ok()..none = true;
|
var ok = client.Response_Ok()..none = true;
|
||||||
return client.Response()..ok = ok;
|
return client.Response()..ok = ok;
|
||||||
|
|
@ -185,8 +219,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
kind: Value(message.kind),
|
kind: Value(message.kind),
|
||||||
messageOtherId: Value(message.messageId),
|
messageOtherId: Value(message.messageId),
|
||||||
contentJson: Value(content),
|
contentJson: Value(content),
|
||||||
downloadState: Value(DownloadState.downloaded),
|
acknowledgeByServer: Value(true),
|
||||||
sendAt: Value(message.timestamp),
|
downloadState: Value(message.kind == MessageKind.media
|
||||||
|
? DownloadState.pending
|
||||||
|
: DownloadState.downloaded),
|
||||||
|
sendAt: Value(message.timestamp.toUtc()),
|
||||||
);
|
);
|
||||||
|
|
||||||
final messageId = await twonlyDatabase.insertMessage(
|
final messageId = await twonlyDatabase.insertMessage(
|
||||||
|
|
@ -198,6 +235,7 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptAndSendMessage(
|
encryptAndSendMessage(
|
||||||
|
message.messageId!,
|
||||||
fromUserId,
|
fromUserId,
|
||||||
MessageJson(
|
MessageJson(
|
||||||
kind: MessageKind.ack,
|
kind: MessageKind.ack,
|
||||||
|
|
@ -220,8 +258,11 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
|
||||||
if (!globalIsAppInBackground) {
|
if (!globalIsAppInBackground) {
|
||||||
final content = message.content;
|
final content = message.content;
|
||||||
if (content is MediaMessageContent) {
|
if (content is MediaMessageContent) {
|
||||||
List<int> downloadToken = content.downloadToken;
|
tryDownloadMedia(
|
||||||
tryDownloadMedia(messageId, fromUserId, downloadToken);
|
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/proto/api/server_to_client.pb.dart' as server;
|
||||||
import 'package:twonly/src/providers/api/api.dart';
|
import 'package:twonly/src/providers/api/api.dart';
|
||||||
import 'package:twonly/src/providers/api/api_utils.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/providers/api/server_messages.dart';
|
||||||
import 'package:twonly/src/services/fcm_service.dart';
|
import 'package:twonly/src/services/fcm_service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
@ -327,8 +328,9 @@ class ApiProvider {
|
||||||
return await sendRequestSync(req);
|
return await sendRequestSync(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result> getUploadToken() async {
|
Future<Result> getUploadToken(int recipientsCount) async {
|
||||||
var get = ApplicationData_GetUploadToken();
|
var get = ApplicationData_GetUploadToken()
|
||||||
|
..recipientsCount = recipientsCount;
|
||||||
var appData = ApplicationData()..getuploadtoken = get;
|
var appData = ApplicationData()..getuploadtoken = get;
|
||||||
var req = createClientToServerFromApplicationData(appData);
|
var req = createClientToServerFromApplicationData(appData);
|
||||||
return await sendRequestSync(req);
|
return await sendRequestSync(req);
|
||||||
|
|
@ -343,12 +345,15 @@ class ApiProvider {
|
||||||
return await sendRequestSync(req);
|
return await sendRequestSync(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Result> uploadData(
|
Future<Result> uploadData(List<int> uploadToken, Uint8List data, int offset,
|
||||||
List<int> uploadToken, Uint8List data, int offset) async {
|
List<int>? checksum) async {
|
||||||
var get = ApplicationData_UploadData()
|
var get = ApplicationData_UploadData()
|
||||||
..uploadToken = uploadToken
|
..uploadToken = uploadToken
|
||||||
..data = data
|
..data = data
|
||||||
..offset = offset;
|
..offset = offset;
|
||||||
|
if (checksum != null) {
|
||||||
|
get.checksum = checksum;
|
||||||
|
}
|
||||||
var appData = ApplicationData()..uploaddata = get;
|
var appData = ApplicationData()..uploaddata = get;
|
||||||
var req = createClientToServerFromApplicationData(appData);
|
var req = createClientToServerFromApplicationData(appData);
|
||||||
final result = await sendRequestSync(req);
|
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/components/notification_badge.dart';
|
||||||
import 'package:twonly/src/database/contacts_db.dart';
|
import 'package:twonly/src/database/contacts_db.dart';
|
||||||
import 'package:twonly/src/database/database.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/providers/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/camera_to_share/share_image_view.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/components/verified_shield.dart';
|
||||||
import 'package:twonly/src/database/contacts_db.dart';
|
import 'package:twonly/src/database/contacts_db.dart';
|
||||||
import 'package:twonly/src/database/database.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/providers/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/home_view.dart';
|
import 'package:twonly/src/views/home_view.dart';
|
||||||
|
|
@ -65,8 +65,12 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
|
|
||||||
//_users = await DbContacts.getActiveUsers();
|
//_users = await DbContacts.getActiveUsers();
|
||||||
// _updateUsers(_users);
|
// _updateUsers(_users);
|
||||||
// imageBytes = await widget.imageBytesFuture;
|
initAsync();
|
||||||
// setState(() {});
|
}
|
||||||
|
|
||||||
|
Future initAsync() async {
|
||||||
|
imageBytes = await widget.imageBytesFuture;
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -219,7 +223,7 @@ class _ShareImageView extends State<ShareImageView> {
|
||||||
setState(() {
|
setState(() {
|
||||||
sendingImage = true;
|
sendingImage = true;
|
||||||
});
|
});
|
||||||
await sendImage(
|
sendImage(
|
||||||
_selectedUserIds.toList(),
|
_selectedUserIds.toList(),
|
||||||
imageBytes!,
|
imageBytes!,
|
||||||
widget.isRealTwonly,
|
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/database/messages_db.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/providers/api/api.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/providers/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:twonly/src/views/chats/media_viewer_view.dart';
|
import 'package:twonly/src/views/chats/media_viewer_view.dart';
|
||||||
|
|
@ -31,19 +32,10 @@ class ChatListEntry extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
bool right = message.messageOtherId == null;
|
bool right = message.messageOtherId == null;
|
||||||
MessageSendState state = messageSendStateFromMessage(message);
|
|
||||||
|
|
||||||
bool isDownloading = false;
|
|
||||||
List<int> token = [];
|
|
||||||
|
|
||||||
MessageContent? content =
|
MessageContent? content =
|
||||||
MessageContent.fromJson(message.kind, jsonDecode(message.contentJson!));
|
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();
|
Widget child = Container();
|
||||||
|
|
||||||
if (content is TextMessageContent) {
|
if (content is TextMessageContent) {
|
||||||
|
|
@ -86,7 +78,7 @@ class ChatListEntry extends StatelessWidget {
|
||||||
|
|
||||||
child = GestureDetector(
|
child = GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (state == MessageSendState.received && !isDownloading) {
|
if (message.kind == MessageKind.media) {
|
||||||
if (message.downloadState == DownloadState.downloaded) {
|
if (message.downloadState == DownloadState.downloaded) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
@ -95,7 +87,7 @@ class ChatListEntry extends StatelessWidget {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
tryDownloadMedia(message.messageId, message.contactId, token,
|
tryDownloadMedia(message.messageId, message.contactId, content,
|
||||||
force: true);
|
force: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:provider/provider.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/database.dart';
|
||||||
import 'package:twonly/src/database/messages_db.dart';
|
import 'package:twonly/src/database/messages_db.dart';
|
||||||
import 'package:twonly/src/model/json/message.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/providers/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||||
|
|
@ -151,7 +152,6 @@ class UserListItem extends StatefulWidget {
|
||||||
class _UserListItem extends State<UserListItem> {
|
class _UserListItem extends State<UserListItem> {
|
||||||
int lastMessageInSeconds = 0;
|
int lastMessageInSeconds = 0;
|
||||||
MessageSendState state = MessageSendState.send;
|
MessageSendState state = MessageSendState.send;
|
||||||
List<int> token = [];
|
|
||||||
Message? currentMessage;
|
Message? currentMessage;
|
||||||
|
|
||||||
Timer? updateTime;
|
Timer? updateTime;
|
||||||
|
|
@ -209,7 +209,14 @@ class _UserListItem extends State<UserListItem> {
|
||||||
var lastMessages = [lastMessage];
|
var lastMessages = [lastMessage];
|
||||||
if (notOpenedMessagesSnapshot.data != null &&
|
if (notOpenedMessagesSnapshot.data != null &&
|
||||||
notOpenedMessagesSnapshot.data!.isNotEmpty) {
|
notOpenedMessagesSnapshot.data!.isNotEmpty) {
|
||||||
|
// filter first for only received messages
|
||||||
|
lastMessages = notOpenedMessagesSnapshot.data!
|
||||||
|
.where((x) => x.messageOtherId != null)
|
||||||
|
.toList();
|
||||||
|
if (lastMessages.isEmpty) {
|
||||||
lastMessages = notOpenedMessagesSnapshot.data!;
|
lastMessages = notOpenedMessagesSnapshot.data!;
|
||||||
|
}
|
||||||
|
|
||||||
var media =
|
var media =
|
||||||
lastMessages.where((x) => x.kind == MessageKind.media);
|
lastMessages.where((x) => x.kind == MessageKind.media);
|
||||||
if (media.isNotEmpty) {
|
if (media.isNotEmpty) {
|
||||||
|
|
@ -253,15 +260,16 @@ class _UserListItem extends State<UserListItem> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Message msg = currentMessage!;
|
Message msg = currentMessage!;
|
||||||
if (msg.downloadState == DownloadState.downloading) {
|
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;
|
return;
|
||||||
}
|
|
||||||
if (msg.downloadState == DownloadState.pending) {
|
case DownloadState.downloaded:
|
||||||
tryDownloadMedia(msg.messageId, msg.contactId, token, force: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state == MessageSendState.received &&
|
|
||||||
msg.kind == MessageKind.media) {
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) {
|
MaterialPageRoute(builder: (context) {
|
||||||
|
|
@ -269,6 +277,10 @@ class _UserListItem extends State<UserListItem> {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:twonly/src/database/database.dart';
|
||||||
import 'package:twonly/src/database/messages_db.dart';
|
import 'package:twonly/src/database/messages_db.dart';
|
||||||
import 'package:twonly/src/model/json/message.dart';
|
import 'package:twonly/src/model/json/message.dart';
|
||||||
import 'package:twonly/src/providers/api/api.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/providers/send_next_media_to.dart';
|
||||||
import 'package:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
|
|
@ -51,13 +52,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
asyncLoadNextMedia();
|
asyncLoadNextMedia(true);
|
||||||
loadCurrentMediaFile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future asyncLoadNextMedia() async {
|
Future asyncLoadNextMedia(bool firstRun) async {
|
||||||
Stream<List<Message>> messages =
|
Stream<List<Message>> messages =
|
||||||
twonlyDatabase.watchMessageNotOpened(widget.userId);
|
twonlyDatabase.watchMediaMessageNotOpened(widget.userId);
|
||||||
|
|
||||||
_subscription = messages.listen((messages) {
|
_subscription = messages.listen((messages) {
|
||||||
for (Message msg in messages) {
|
for (Message msg in messages) {
|
||||||
|
|
@ -66,6 +66,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
if (firstRun) {
|
||||||
|
loadCurrentMediaFile();
|
||||||
|
firstRun = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,24 +121,21 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
flutterLocalNotificationsPlugin.cancel(current.messageId);
|
flutterLocalNotificationsPlugin.cancel(current.messageId);
|
||||||
if (current.downloadState == DownloadState.pending) {
|
if (current.downloadState == DownloadState.pending) {
|
||||||
setState(() {
|
setState(() {
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
});
|
});
|
||||||
await tryDownloadMedia(
|
await tryDownloadMedia(current.messageId, current.contactId, content,
|
||||||
current.messageId, current.contactId, content.downloadToken,
|
|
||||||
force: true);
|
force: true);
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
if (isDownloading) {
|
if (isDownloading) {
|
||||||
await Future.delayed(Duration(milliseconds: 10));
|
await Future.delayed(Duration(milliseconds: 10));
|
||||||
}
|
}
|
||||||
imageBytes = await getDownloadedMedia(
|
if (content.downloadToken == null) break;
|
||||||
content.downloadToken,
|
imageBytes = await getDownloadedMedia(current, content.downloadToken!);
|
||||||
current.messageOtherId!,
|
|
||||||
current.contactId,
|
|
||||||
);
|
|
||||||
} while (isDownloading && imageBytes == null);
|
} while (isDownloading && imageBytes == null);
|
||||||
|
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
|
@ -153,7 +154,6 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
startTimer() {
|
startTimer() {
|
||||||
nextMediaTimer?.cancel();
|
nextMediaTimer?.cancel();
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
|
||||||
if (added > 0) {
|
if (added > 0) {
|
||||||
if (await SignalHelper.addNewContact(res.value.userdata)) {
|
if (await SignalHelper.addNewContact(res.value.userdata)) {
|
||||||
encryptAndSendMessage(
|
encryptAndSendMessage(
|
||||||
|
null,
|
||||||
res.value.userdata.userId.toInt(),
|
res.value.userdata.userId.toInt(),
|
||||||
MessageJson(
|
MessageJson(
|
||||||
kind: MessageKind.contactRequest,
|
kind: MessageKind.contactRequest,
|
||||||
|
|
@ -207,6 +208,7 @@ class _ContactsListViewState extends State<ContactsListView> {
|
||||||
await twonlyDatabase
|
await twonlyDatabase
|
||||||
.deleteContactByUserId(contact.userId);
|
.deleteContactByUserId(contact.userId);
|
||||||
encryptAndSendMessage(
|
encryptAndSendMessage(
|
||||||
|
null,
|
||||||
contact.userId,
|
contact.userId,
|
||||||
MessageJson(
|
MessageJson(
|
||||||
kind: MessageKind.rejectRequest,
|
kind: MessageKind.rejectRequest,
|
||||||
|
|
@ -223,6 +225,7 @@ class _ContactsListViewState extends State<ContactsListView> {
|
||||||
final update = ContactsCompanion(accepted: Value(true));
|
final update = ContactsCompanion(accepted: Value(true));
|
||||||
await twonlyDatabase.updateContact(contact.userId, update);
|
await twonlyDatabase.updateContact(contact.userId, update);
|
||||||
encryptAndSendMessage(
|
encryptAndSendMessage(
|
||||||
|
null,
|
||||||
contact.userId,
|
contact.userId,
|
||||||
MessageJson(
|
MessageJson(
|
||||||
kind: MessageKind.acceptRequest,
|
kind: MessageKind.acceptRequest,
|
||||||
|
|
|
||||||
56
pubspec.lock
56
pubspec.lock
|
|
@ -249,6 +249,22 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
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:
|
cv:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -567,50 +583,50 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
name: flutter_secure_storage
|
||||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
sha256: f7eceb0bc6f4fd0441e29d43cab9ac2a1c5ffd7ea7b64075136b718c46954874
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
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:
|
flutter_secure_storage_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_linux
|
name: flutter_secure_storage_linux
|
||||||
sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705
|
sha256: "9b4b73127e857cd3117d43a70fa3dddadb6e0b253be62e6a6ab85caa0742182c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "2.0.1"
|
||||||
flutter_secure_storage_macos:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_secure_storage_macos
|
|
||||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.3"
|
|
||||||
flutter_secure_storage_platform_interface:
|
flutter_secure_storage_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_platform_interface
|
name: flutter_secure_storage_platform_interface
|
||||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "2.0.1"
|
||||||
flutter_secure_storage_web:
|
flutter_secure_storage_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_web
|
name: flutter_secure_storage_web
|
||||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
sha256: "4c3f233e739545c6cb09286eeec1cc4744138372b985113acc904f7263bef517"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "2.0.0"
|
||||||
flutter_secure_storage_windows:
|
flutter_secure_storage_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage_windows
|
name: flutter_secure_storage_windows
|
||||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
sha256: ff32af20f70a8d0e59b2938fc92de35b54a74671041c814275afd80e27df9f21
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "4.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -745,10 +761,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: js
|
name: js
|
||||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.7"
|
version: "0.7.1"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ dependencies:
|
||||||
flutter_local_notifications: ^18.0.1
|
flutter_local_notifications: ^18.0.1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^10.0.0-beta.4
|
||||||
font_awesome_flutter: ^10.8.0
|
font_awesome_flutter: ^10.8.0
|
||||||
gal: ^2.3.1
|
gal: ^2.3.1
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
|
|
@ -50,6 +50,8 @@ dependencies:
|
||||||
permission_handler: ^11.3.1
|
permission_handler: ^11.3.1
|
||||||
pie_menu: ^3.2.7
|
pie_menu: ^3.2.7
|
||||||
protobuf: ^2.1.0
|
protobuf: ^2.1.0
|
||||||
|
cryptography_plus: ^2.7.0
|
||||||
|
cryptography_flutter_plus: ^2.3.2
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
reorderables: ^0.6.0
|
reorderables: ^0.6.0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue