mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 09:08:40 +00:00
media upload
This commit is contained in:
parent
b2dc384465
commit
645dfe16da
20 changed files with 456 additions and 647 deletions
|
|
@ -10,11 +10,9 @@ import 'package:twonly/src/providers/connection.provider.dart';
|
|||
import 'package:twonly/src/providers/image_editor.provider.dart';
|
||||
import 'package:twonly/src/providers/settings.provider.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/media_background.service.dart';
|
||||
import 'package:twonly/src/services/fcm.service.dart';
|
||||
import 'package:twonly/src/services/notifications/setup.notifications.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -26,4 +26,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
|
|||
await (update(groups)..where((c) => c.groupId.equals(groupId)))
|
||||
.write(updates);
|
||||
}
|
||||
|
||||
Future<List<GroupMember>> getGroupMembers(String groupId) async {
|
||||
return (select(groupMembers)..where((t) => t.groupId.equals(groupId)))
|
||||
.get();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,6 +285,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
|||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future<void> updateMessagesByMediaId(
|
||||
String mediaId,
|
||||
MessagesCompanion updatedValues,
|
||||
) {
|
||||
return (update(messages)..where((c) => c.mediaId.equals(mediaId)))
|
||||
.write(updatedValues);
|
||||
}
|
||||
|
||||
Future<Message?> insertMessage(MessagesCompanion message) async {
|
||||
try {
|
||||
final rowId = await into(messages).insert(message);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ class Groups extends Table {
|
|||
BoolColumn get pinned => boolean().withDefault(const Constant(false))();
|
||||
BoolColumn get archived => boolean().withDefault(const Constant(false))();
|
||||
|
||||
TextColumn get groupName => text()();
|
||||
|
||||
DateTimeColumn get lastMessageExchange =>
|
||||
dateTime().withDefault(currentDateAndTime)();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ enum UploadState {
|
|||
// Image was stored but not send
|
||||
storedOnly,
|
||||
// At this point the user is finished with editing, and the media file can be uploaded
|
||||
compressing,
|
||||
encrypting,
|
||||
preprocessing,
|
||||
uploading,
|
||||
backgroundUploadTaskStarted,
|
||||
uploaded,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class Messages extends Table {
|
|||
TextColumn get mediaId =>
|
||||
text().nullable().references(MediaFiles, #mediaId)();
|
||||
|
||||
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();
|
||||
|
||||
BlobColumn get downloadToken => blob().nullable()();
|
||||
|
||||
TextColumn get quotesMessageId =>
|
||||
|
|
|
|||
|
|
@ -1061,6 +1061,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
|
|||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'),
|
||||
defaultValue: const Constant(false));
|
||||
static const VerificationMeta _groupNameMeta =
|
||||
const VerificationMeta('groupName');
|
||||
@override
|
||||
late final GeneratedColumn<String> groupName = GeneratedColumn<String>(
|
||||
'group_name', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const VerificationMeta _lastMessageExchangeMeta =
|
||||
const VerificationMeta('lastMessageExchange');
|
||||
@override
|
||||
|
|
@ -1084,6 +1090,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
|
|||
isGroupOfTwo,
|
||||
pinned,
|
||||
archived,
|
||||
groupName,
|
||||
lastMessageExchange,
|
||||
createdAt
|
||||
];
|
||||
|
|
@ -1125,6 +1132,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
|
|||
context.handle(_archivedMeta,
|
||||
archived.isAcceptableOrUnknown(data['archived']!, _archivedMeta));
|
||||
}
|
||||
if (data.containsKey('group_name')) {
|
||||
context.handle(_groupNameMeta,
|
||||
groupName.isAcceptableOrUnknown(data['group_name']!, _groupNameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_groupNameMeta);
|
||||
}
|
||||
if (data.containsKey('last_message_exchange')) {
|
||||
context.handle(
|
||||
_lastMessageExchangeMeta,
|
||||
|
|
@ -1154,6 +1167,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
|
|||
.read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!,
|
||||
archived: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.bool, data['${effectivePrefix}archived'])!,
|
||||
groupName: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}group_name'])!,
|
||||
lastMessageExchange: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}last_message_exchange'])!,
|
||||
|
|
@ -1174,6 +1189,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
final bool isGroupOfTwo;
|
||||
final bool pinned;
|
||||
final bool archived;
|
||||
final String groupName;
|
||||
final DateTime lastMessageExchange;
|
||||
final DateTime createdAt;
|
||||
const Group(
|
||||
|
|
@ -1182,6 +1198,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
required this.isGroupOfTwo,
|
||||
required this.pinned,
|
||||
required this.archived,
|
||||
required this.groupName,
|
||||
required this.lastMessageExchange,
|
||||
required this.createdAt});
|
||||
@override
|
||||
|
|
@ -1192,6 +1209,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
map['is_group_of_two'] = Variable<bool>(isGroupOfTwo);
|
||||
map['pinned'] = Variable<bool>(pinned);
|
||||
map['archived'] = Variable<bool>(archived);
|
||||
map['group_name'] = Variable<String>(groupName);
|
||||
map['last_message_exchange'] = Variable<DateTime>(lastMessageExchange);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
return map;
|
||||
|
|
@ -1204,6 +1222,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
isGroupOfTwo: Value(isGroupOfTwo),
|
||||
pinned: Value(pinned),
|
||||
archived: Value(archived),
|
||||
groupName: Value(groupName),
|
||||
lastMessageExchange: Value(lastMessageExchange),
|
||||
createdAt: Value(createdAt),
|
||||
);
|
||||
|
|
@ -1218,6 +1237,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
isGroupOfTwo: serializer.fromJson<bool>(json['isGroupOfTwo']),
|
||||
pinned: serializer.fromJson<bool>(json['pinned']),
|
||||
archived: serializer.fromJson<bool>(json['archived']),
|
||||
groupName: serializer.fromJson<String>(json['groupName']),
|
||||
lastMessageExchange:
|
||||
serializer.fromJson<DateTime>(json['lastMessageExchange']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
|
|
@ -1232,6 +1252,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
'isGroupOfTwo': serializer.toJson<bool>(isGroupOfTwo),
|
||||
'pinned': serializer.toJson<bool>(pinned),
|
||||
'archived': serializer.toJson<bool>(archived),
|
||||
'groupName': serializer.toJson<String>(groupName),
|
||||
'lastMessageExchange': serializer.toJson<DateTime>(lastMessageExchange),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
};
|
||||
|
|
@ -1243,6 +1264,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
bool? isGroupOfTwo,
|
||||
bool? pinned,
|
||||
bool? archived,
|
||||
String? groupName,
|
||||
DateTime? lastMessageExchange,
|
||||
DateTime? createdAt}) =>
|
||||
Group(
|
||||
|
|
@ -1251,6 +1273,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo,
|
||||
pinned: pinned ?? this.pinned,
|
||||
archived: archived ?? this.archived,
|
||||
groupName: groupName ?? this.groupName,
|
||||
lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
|
|
@ -1265,6 +1288,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
: this.isGroupOfTwo,
|
||||
pinned: data.pinned.present ? data.pinned.value : this.pinned,
|
||||
archived: data.archived.present ? data.archived.value : this.archived,
|
||||
groupName: data.groupName.present ? data.groupName.value : this.groupName,
|
||||
lastMessageExchange: data.lastMessageExchange.present
|
||||
? data.lastMessageExchange.value
|
||||
: this.lastMessageExchange,
|
||||
|
|
@ -1280,6 +1304,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
..write('isGroupOfTwo: $isGroupOfTwo, ')
|
||||
..write('pinned: $pinned, ')
|
||||
..write('archived: $archived, ')
|
||||
..write('groupName: $groupName, ')
|
||||
..write('lastMessageExchange: $lastMessageExchange, ')
|
||||
..write('createdAt: $createdAt')
|
||||
..write(')'))
|
||||
|
|
@ -1288,7 +1313,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
|
||||
@override
|
||||
int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned,
|
||||
archived, lastMessageExchange, createdAt);
|
||||
archived, groupName, lastMessageExchange, createdAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
|
|
@ -1298,6 +1323,7 @@ class Group extends DataClass implements Insertable<Group> {
|
|||
other.isGroupOfTwo == this.isGroupOfTwo &&
|
||||
other.pinned == this.pinned &&
|
||||
other.archived == this.archived &&
|
||||
other.groupName == this.groupName &&
|
||||
other.lastMessageExchange == this.lastMessageExchange &&
|
||||
other.createdAt == this.createdAt);
|
||||
}
|
||||
|
|
@ -1308,6 +1334,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
final Value<bool> isGroupOfTwo;
|
||||
final Value<bool> pinned;
|
||||
final Value<bool> archived;
|
||||
final Value<String> groupName;
|
||||
final Value<DateTime> lastMessageExchange;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<int> rowid;
|
||||
|
|
@ -1317,6 +1344,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
this.isGroupOfTwo = const Value.absent(),
|
||||
this.pinned = const Value.absent(),
|
||||
this.archived = const Value.absent(),
|
||||
this.groupName = const Value.absent(),
|
||||
this.lastMessageExchange = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
|
|
@ -1327,17 +1355,20 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
required bool isGroupOfTwo,
|
||||
this.pinned = const Value.absent(),
|
||||
this.archived = const Value.absent(),
|
||||
required String groupName,
|
||||
this.lastMessageExchange = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : isGroupAdmin = Value(isGroupAdmin),
|
||||
isGroupOfTwo = Value(isGroupOfTwo);
|
||||
isGroupOfTwo = Value(isGroupOfTwo),
|
||||
groupName = Value(groupName);
|
||||
static Insertable<Group> custom({
|
||||
Expression<String>? groupId,
|
||||
Expression<bool>? isGroupAdmin,
|
||||
Expression<bool>? isGroupOfTwo,
|
||||
Expression<bool>? pinned,
|
||||
Expression<bool>? archived,
|
||||
Expression<String>? groupName,
|
||||
Expression<DateTime>? lastMessageExchange,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<int>? rowid,
|
||||
|
|
@ -1348,6 +1379,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo,
|
||||
if (pinned != null) 'pinned': pinned,
|
||||
if (archived != null) 'archived': archived,
|
||||
if (groupName != null) 'group_name': groupName,
|
||||
if (lastMessageExchange != null)
|
||||
'last_message_exchange': lastMessageExchange,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
|
|
@ -1361,6 +1393,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
Value<bool>? isGroupOfTwo,
|
||||
Value<bool>? pinned,
|
||||
Value<bool>? archived,
|
||||
Value<String>? groupName,
|
||||
Value<DateTime>? lastMessageExchange,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<int>? rowid}) {
|
||||
|
|
@ -1370,6 +1403,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo,
|
||||
pinned: pinned ?? this.pinned,
|
||||
archived: archived ?? this.archived,
|
||||
groupName: groupName ?? this.groupName,
|
||||
lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
rowid: rowid ?? this.rowid,
|
||||
|
|
@ -1394,6 +1428,9 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
if (archived.present) {
|
||||
map['archived'] = Variable<bool>(archived.value);
|
||||
}
|
||||
if (groupName.present) {
|
||||
map['group_name'] = Variable<String>(groupName.value);
|
||||
}
|
||||
if (lastMessageExchange.present) {
|
||||
map['last_message_exchange'] =
|
||||
Variable<DateTime>(lastMessageExchange.value);
|
||||
|
|
@ -1415,6 +1452,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
|
|||
..write('isGroupOfTwo: $isGroupOfTwo, ')
|
||||
..write('pinned: $pinned, ')
|
||||
..write('archived: $archived, ')
|
||||
..write('groupName: $groupName, ')
|
||||
..write('lastMessageExchange: $lastMessageExchange, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('rowid: $rowid')
|
||||
|
|
@ -2231,6 +2269,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES media_files (media_id)'));
|
||||
static const VerificationMeta _mediaStoredMeta =
|
||||
const VerificationMeta('mediaStored');
|
||||
@override
|
||||
late final GeneratedColumn<bool> mediaStored = GeneratedColumn<bool>(
|
||||
'media_stored', aliasedName, false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("media_stored" IN (0, 1))'),
|
||||
defaultValue: const Constant(false));
|
||||
static const VerificationMeta _downloadTokenMeta =
|
||||
const VerificationMeta('downloadToken');
|
||||
@override
|
||||
|
|
@ -2323,6 +2371,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
senderId,
|
||||
content,
|
||||
mediaId,
|
||||
mediaStored,
|
||||
downloadToken,
|
||||
quotesMessageId,
|
||||
isDeletedFromSender,
|
||||
|
|
@ -2366,6 +2415,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
context.handle(_mediaIdMeta,
|
||||
mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta));
|
||||
}
|
||||
if (data.containsKey('media_stored')) {
|
||||
context.handle(
|
||||
_mediaStoredMeta,
|
||||
mediaStored.isAcceptableOrUnknown(
|
||||
data['media_stored']!, _mediaStoredMeta));
|
||||
}
|
||||
if (data.containsKey('download_token')) {
|
||||
context.handle(
|
||||
_downloadTokenMeta,
|
||||
|
|
@ -2439,6 +2494,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
|
|||
.read(DriftSqlType.string, data['${effectivePrefix}content']),
|
||||
mediaId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}media_id']),
|
||||
mediaStored: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!,
|
||||
downloadToken: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.blob, data['${effectivePrefix}download_token']),
|
||||
quotesMessageId: attachedDatabase.typeMapping.read(
|
||||
|
|
@ -2474,6 +2531,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
final int? senderId;
|
||||
final String? content;
|
||||
final String? mediaId;
|
||||
final bool mediaStored;
|
||||
final Uint8List? downloadToken;
|
||||
final String? quotesMessageId;
|
||||
final bool isDeletedFromSender;
|
||||
|
|
@ -2490,6 +2548,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
this.senderId,
|
||||
this.content,
|
||||
this.mediaId,
|
||||
required this.mediaStored,
|
||||
this.downloadToken,
|
||||
this.quotesMessageId,
|
||||
required this.isDeletedFromSender,
|
||||
|
|
@ -2514,6 +2573,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
if (!nullToAbsent || mediaId != null) {
|
||||
map['media_id'] = Variable<String>(mediaId);
|
||||
}
|
||||
map['media_stored'] = Variable<bool>(mediaStored);
|
||||
if (!nullToAbsent || downloadToken != null) {
|
||||
map['download_token'] = Variable<Uint8List>(downloadToken);
|
||||
}
|
||||
|
|
@ -2548,6 +2608,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
mediaId: mediaId == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(mediaId),
|
||||
mediaStored: Value(mediaStored),
|
||||
downloadToken: downloadToken == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(downloadToken),
|
||||
|
|
@ -2578,6 +2639,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
senderId: serializer.fromJson<int?>(json['senderId']),
|
||||
content: serializer.fromJson<String?>(json['content']),
|
||||
mediaId: serializer.fromJson<String?>(json['mediaId']),
|
||||
mediaStored: serializer.fromJson<bool>(json['mediaStored']),
|
||||
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
|
||||
quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']),
|
||||
isDeletedFromSender:
|
||||
|
|
@ -2600,6 +2662,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
'senderId': serializer.toJson<int?>(senderId),
|
||||
'content': serializer.toJson<String?>(content),
|
||||
'mediaId': serializer.toJson<String?>(mediaId),
|
||||
'mediaStored': serializer.toJson<bool>(mediaStored),
|
||||
'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
|
||||
'quotesMessageId': serializer.toJson<String?>(quotesMessageId),
|
||||
'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender),
|
||||
|
|
@ -2619,6 +2682,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
Value<int?> senderId = const Value.absent(),
|
||||
Value<String?> content = const Value.absent(),
|
||||
Value<String?> mediaId = const Value.absent(),
|
||||
bool? mediaStored,
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
Value<String?> quotesMessageId = const Value.absent(),
|
||||
bool? isDeletedFromSender,
|
||||
|
|
@ -2635,6 +2699,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
senderId: senderId.present ? senderId.value : this.senderId,
|
||||
content: content.present ? content.value : this.content,
|
||||
mediaId: mediaId.present ? mediaId.value : this.mediaId,
|
||||
mediaStored: mediaStored ?? this.mediaStored,
|
||||
downloadToken:
|
||||
downloadToken.present ? downloadToken.value : this.downloadToken,
|
||||
quotesMessageId: quotesMessageId.present
|
||||
|
|
@ -2656,6 +2721,8 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
senderId: data.senderId.present ? data.senderId.value : this.senderId,
|
||||
content: data.content.present ? data.content.value : this.content,
|
||||
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
|
||||
mediaStored:
|
||||
data.mediaStored.present ? data.mediaStored.value : this.mediaStored,
|
||||
downloadToken: data.downloadToken.present
|
||||
? data.downloadToken.value
|
||||
: this.downloadToken,
|
||||
|
|
@ -2687,6 +2754,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
..write('senderId: $senderId, ')
|
||||
..write('content: $content, ')
|
||||
..write('mediaId: $mediaId, ')
|
||||
..write('mediaStored: $mediaStored, ')
|
||||
..write('downloadToken: $downloadToken, ')
|
||||
..write('quotesMessageId: $quotesMessageId, ')
|
||||
..write('isDeletedFromSender: $isDeletedFromSender, ')
|
||||
|
|
@ -2708,6 +2776,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
senderId,
|
||||
content,
|
||||
mediaId,
|
||||
mediaStored,
|
||||
$driftBlobEquality.hash(downloadToken),
|
||||
quotesMessageId,
|
||||
isDeletedFromSender,
|
||||
|
|
@ -2727,6 +2796,7 @@ class Message extends DataClass implements Insertable<Message> {
|
|||
other.senderId == this.senderId &&
|
||||
other.content == this.content &&
|
||||
other.mediaId == this.mediaId &&
|
||||
other.mediaStored == this.mediaStored &&
|
||||
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
|
||||
other.quotesMessageId == this.quotesMessageId &&
|
||||
other.isDeletedFromSender == this.isDeletedFromSender &&
|
||||
|
|
@ -2745,6 +2815,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
final Value<int?> senderId;
|
||||
final Value<String?> content;
|
||||
final Value<String?> mediaId;
|
||||
final Value<bool> mediaStored;
|
||||
final Value<Uint8List?> downloadToken;
|
||||
final Value<String?> quotesMessageId;
|
||||
final Value<bool> isDeletedFromSender;
|
||||
|
|
@ -2762,6 +2833,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
this.senderId = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.mediaId = const Value.absent(),
|
||||
this.mediaStored = const Value.absent(),
|
||||
this.downloadToken = const Value.absent(),
|
||||
this.quotesMessageId = const Value.absent(),
|
||||
this.isDeletedFromSender = const Value.absent(),
|
||||
|
|
@ -2780,6 +2852,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
this.senderId = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.mediaId = const Value.absent(),
|
||||
this.mediaStored = const Value.absent(),
|
||||
this.downloadToken = const Value.absent(),
|
||||
this.quotesMessageId = const Value.absent(),
|
||||
this.isDeletedFromSender = const Value.absent(),
|
||||
|
|
@ -2798,6 +2871,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
Expression<int>? senderId,
|
||||
Expression<String>? content,
|
||||
Expression<String>? mediaId,
|
||||
Expression<bool>? mediaStored,
|
||||
Expression<Uint8List>? downloadToken,
|
||||
Expression<String>? quotesMessageId,
|
||||
Expression<bool>? isDeletedFromSender,
|
||||
|
|
@ -2816,6 +2890,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
if (senderId != null) 'sender_id': senderId,
|
||||
if (content != null) 'content': content,
|
||||
if (mediaId != null) 'media_id': mediaId,
|
||||
if (mediaStored != null) 'media_stored': mediaStored,
|
||||
if (downloadToken != null) 'download_token': downloadToken,
|
||||
if (quotesMessageId != null) 'quotes_message_id': quotesMessageId,
|
||||
if (isDeletedFromSender != null)
|
||||
|
|
@ -2837,6 +2912,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
Value<int?>? senderId,
|
||||
Value<String?>? content,
|
||||
Value<String?>? mediaId,
|
||||
Value<bool>? mediaStored,
|
||||
Value<Uint8List?>? downloadToken,
|
||||
Value<String?>? quotesMessageId,
|
||||
Value<bool>? isDeletedFromSender,
|
||||
|
|
@ -2854,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
senderId: senderId ?? this.senderId,
|
||||
content: content ?? this.content,
|
||||
mediaId: mediaId ?? this.mediaId,
|
||||
mediaStored: mediaStored ?? this.mediaStored,
|
||||
downloadToken: downloadToken ?? this.downloadToken,
|
||||
quotesMessageId: quotesMessageId ?? this.quotesMessageId,
|
||||
isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender,
|
||||
|
|
@ -2886,6 +2963,9 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
if (mediaId.present) {
|
||||
map['media_id'] = Variable<String>(mediaId.value);
|
||||
}
|
||||
if (mediaStored.present) {
|
||||
map['media_stored'] = Variable<bool>(mediaStored.value);
|
||||
}
|
||||
if (downloadToken.present) {
|
||||
map['download_token'] = Variable<Uint8List>(downloadToken.value);
|
||||
}
|
||||
|
|
@ -2930,6 +3010,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
|
|||
..write('senderId: $senderId, ')
|
||||
..write('content: $content, ')
|
||||
..write('mediaId: $mediaId, ')
|
||||
..write('mediaStored: $mediaStored, ')
|
||||
..write('downloadToken: $downloadToken, ')
|
||||
..write('quotesMessageId: $quotesMessageId, ')
|
||||
..write('isDeletedFromSender: $isDeletedFromSender, ')
|
||||
|
|
@ -6863,6 +6944,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({
|
|||
required bool isGroupOfTwo,
|
||||
Value<bool> pinned,
|
||||
Value<bool> archived,
|
||||
required String groupName,
|
||||
Value<DateTime> lastMessageExchange,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int> rowid,
|
||||
|
|
@ -6873,6 +6955,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({
|
|||
Value<bool> isGroupOfTwo,
|
||||
Value<bool> pinned,
|
||||
Value<bool> archived,
|
||||
Value<String> groupName,
|
||||
Value<DateTime> lastMessageExchange,
|
||||
Value<DateTime> createdAt,
|
||||
Value<int> rowid,
|
||||
|
|
@ -6921,6 +7004,9 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> {
|
|||
ColumnFilters<bool> get archived => $composableBuilder(
|
||||
column: $table.archived, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get groupName => $composableBuilder(
|
||||
column: $table.groupName, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get lastMessageExchange => $composableBuilder(
|
||||
column: $table.lastMessageExchange,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
|
|
@ -6975,6 +7061,9 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> {
|
|||
ColumnOrderings<bool> get archived => $composableBuilder(
|
||||
column: $table.archived, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get groupName => $composableBuilder(
|
||||
column: $table.groupName, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get lastMessageExchange => $composableBuilder(
|
||||
column: $table.lastMessageExchange,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
|
@ -7007,6 +7096,9 @@ class $$GroupsTableAnnotationComposer
|
|||
GeneratedColumn<bool> get archived =>
|
||||
$composableBuilder(column: $table.archived, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get groupName =>
|
||||
$composableBuilder(column: $table.groupName, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get lastMessageExchange => $composableBuilder(
|
||||
column: $table.lastMessageExchange, builder: (column) => column);
|
||||
|
||||
|
|
@ -7063,6 +7155,7 @@ class $$GroupsTableTableManager extends RootTableManager<
|
|||
Value<bool> isGroupOfTwo = const Value.absent(),
|
||||
Value<bool> pinned = const Value.absent(),
|
||||
Value<bool> archived = const Value.absent(),
|
||||
Value<String> groupName = const Value.absent(),
|
||||
Value<DateTime> lastMessageExchange = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
|
|
@ -7073,6 +7166,7 @@ class $$GroupsTableTableManager extends RootTableManager<
|
|||
isGroupOfTwo: isGroupOfTwo,
|
||||
pinned: pinned,
|
||||
archived: archived,
|
||||
groupName: groupName,
|
||||
lastMessageExchange: lastMessageExchange,
|
||||
createdAt: createdAt,
|
||||
rowid: rowid,
|
||||
|
|
@ -7083,6 +7177,7 @@ class $$GroupsTableTableManager extends RootTableManager<
|
|||
required bool isGroupOfTwo,
|
||||
Value<bool> pinned = const Value.absent(),
|
||||
Value<bool> archived = const Value.absent(),
|
||||
required String groupName,
|
||||
Value<DateTime> lastMessageExchange = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
|
|
@ -7093,6 +7188,7 @@ class $$GroupsTableTableManager extends RootTableManager<
|
|||
isGroupOfTwo: isGroupOfTwo,
|
||||
pinned: pinned,
|
||||
archived: archived,
|
||||
groupName: groupName,
|
||||
lastMessageExchange: lastMessageExchange,
|
||||
createdAt: createdAt,
|
||||
rowid: rowid,
|
||||
|
|
@ -7556,6 +7652,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
|
|||
Value<int?> senderId,
|
||||
Value<String?> content,
|
||||
Value<String?> mediaId,
|
||||
Value<bool> mediaStored,
|
||||
Value<Uint8List?> downloadToken,
|
||||
Value<String?> quotesMessageId,
|
||||
Value<bool> isDeletedFromSender,
|
||||
|
|
@ -7574,6 +7671,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
|
|||
Value<int?> senderId,
|
||||
Value<String?> content,
|
||||
Value<String?> mediaId,
|
||||
Value<bool> mediaStored,
|
||||
Value<Uint8List?> downloadToken,
|
||||
Value<String?> quotesMessageId,
|
||||
Value<bool> isDeletedFromSender,
|
||||
|
|
@ -7716,6 +7814,9 @@ class $$MessagesTableFilterComposer
|
|||
ColumnFilters<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<bool> get mediaStored => $composableBuilder(
|
||||
column: $table.mediaStored, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<Uint8List> get downloadToken => $composableBuilder(
|
||||
column: $table.downloadToken, builder: (column) => ColumnFilters(column));
|
||||
|
||||
|
|
@ -7904,6 +8005,9 @@ class $$MessagesTableOrderingComposer
|
|||
ColumnOrderings<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<bool> get mediaStored => $composableBuilder(
|
||||
column: $table.mediaStored, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<Uint8List> get downloadToken => $composableBuilder(
|
||||
column: $table.downloadToken,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
|
@ -8030,6 +8134,9 @@ class $$MessagesTableAnnotationComposer
|
|||
GeneratedColumn<String> get content =>
|
||||
$composableBuilder(column: $table.content, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> get mediaStored => $composableBuilder(
|
||||
column: $table.mediaStored, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<Uint8List> get downloadToken => $composableBuilder(
|
||||
column: $table.downloadToken, builder: (column) => column);
|
||||
|
||||
|
|
@ -8236,6 +8343,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
Value<int?> senderId = const Value.absent(),
|
||||
Value<String?> content = const Value.absent(),
|
||||
Value<String?> mediaId = const Value.absent(),
|
||||
Value<bool> mediaStored = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
Value<String?> quotesMessageId = const Value.absent(),
|
||||
Value<bool> isDeletedFromSender = const Value.absent(),
|
||||
|
|
@ -8254,6 +8362,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
senderId: senderId,
|
||||
content: content,
|
||||
mediaId: mediaId,
|
||||
mediaStored: mediaStored,
|
||||
downloadToken: downloadToken,
|
||||
quotesMessageId: quotesMessageId,
|
||||
isDeletedFromSender: isDeletedFromSender,
|
||||
|
|
@ -8272,6 +8381,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
Value<int?> senderId = const Value.absent(),
|
||||
Value<String?> content = const Value.absent(),
|
||||
Value<String?> mediaId = const Value.absent(),
|
||||
Value<bool> mediaStored = const Value.absent(),
|
||||
Value<Uint8List?> downloadToken = const Value.absent(),
|
||||
Value<String?> quotesMessageId = const Value.absent(),
|
||||
Value<bool> isDeletedFromSender = const Value.absent(),
|
||||
|
|
@ -8290,6 +8400,7 @@ class $$MessagesTableTableManager extends RootTableManager<
|
|||
senderId: senderId,
|
||||
content: content,
|
||||
mediaId: mediaId,
|
||||
mediaStored: mediaStored,
|
||||
downloadToken: downloadToken,
|
||||
quotesMessageId: quotesMessageId,
|
||||
isDeletedFromSender: isDeletedFromSender,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
|
|||
as server;
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/api/server_messages.dart';
|
||||
import 'package:twonly/src/services/api/utils.dart';
|
||||
|
|
@ -94,7 +93,6 @@ class ApiService {
|
|||
if (!globalIsAppInBackground) {
|
||||
unawaited(retransmitRawBytes());
|
||||
unawaited(tryTransmitMessages());
|
||||
unawaited(retryMediaUpload(false));
|
||||
unawaited(tryDownloadAllMediaFiles());
|
||||
unawaited(notifyContactsAboutProfileChange());
|
||||
twonlyDB.markUpdated();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
|
||||
|
|
@ -48,3 +53,59 @@ Future<void> initFileDownloader() async {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||
final mediaId = update.task.taskId.replaceAll('upload_', '');
|
||||
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
|
||||
|
||||
if (media == null) {
|
||||
Log.error(
|
||||
'Got an upload task but no upload media in the media upload database',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.status == TaskStatus.complete) {
|
||||
if (update.responseStatusCode == 200) {
|
||||
Log.info('Upload of ${media.mediaId} success!');
|
||||
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaId,
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.uploaded),
|
||||
),
|
||||
);
|
||||
|
||||
await twonlyDB.messagesDao.updateMessagesByMediaId(
|
||||
media.mediaId,
|
||||
const MessagesCompanion(
|
||||
ackByServer: Value(true),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Log.error(
|
||||
'Got HTTP error ${update.responseStatusCode} for $mediaId',
|
||||
);
|
||||
|
||||
if (update.responseStatusCode == 429) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaId,
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.uploadLimitReached),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.info(
|
||||
'Background upload failed for $mediaId with status ${update.status}. Trying again.',
|
||||
);
|
||||
|
||||
final mediaService = await MediaFileService.fromMedia(media);
|
||||
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
// In all other cases just try the upload again...
|
||||
await startBackgroundMediaUpload(mediaService);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,124 +1,21 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
|
||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/message_old.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
|
||||
import 'package:twonly/src/services/api/messages.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/services/signal/encryption.signal.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
/// States:
|
||||
/// when user recorded an video
|
||||
/// 1. Compress video
|
||||
/// when user clicked the send button (direct send) or share with
|
||||
/// 2. Encrypt media files
|
||||
/// 3. Upload media files
|
||||
/// click send button
|
||||
/// 4. Finalize upload by websocket -> get download tokens
|
||||
/// 5. Send all users the message
|
||||
|
||||
/// Create a new entry in the database
|
||||
|
||||
// Future<bool> checkForFailedUploads() async {
|
||||
// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
|
||||
// final mediaUploadIds = <int>[];
|
||||
// for (final message in messages) {
|
||||
// if (mediaUploadIds.contains(message.mediaUploadId)) {
|
||||
// continue;
|
||||
// }
|
||||
// final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
// message.mediaUploadId!,
|
||||
// const MediaUploadsCompanion(
|
||||
// state: Value(UploadState.pending),
|
||||
// encryptionData: Value(
|
||||
// null, // start from scratch e.q. encrypt the files again if already happen
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// if (affectedRows == 0) {
|
||||
// Log.error(
|
||||
// 'The media from message ${message.messageId} already deleted.',
|
||||
// );
|
||||
// await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
// message.messageId,
|
||||
// const MessagesCompanion(
|
||||
// errorWhileSending: Value(true),
|
||||
// ),
|
||||
// );
|
||||
// } else {
|
||||
// mediaUploadIds.add(message.mediaUploadId!);
|
||||
// }
|
||||
// }
|
||||
// if (messages.isNotEmpty) {
|
||||
// Log.error(
|
||||
// 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.',
|
||||
// );
|
||||
// }
|
||||
// return mediaUploadIds.isNotEmpty; // return true if there are affected
|
||||
// }
|
||||
|
||||
final lockingHandleMediaFile = Mutex();
|
||||
Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
|
||||
if (maxRetries == 0) {
|
||||
Log.error('retried media upload 3 times. abort retrying');
|
||||
return;
|
||||
}
|
||||
final retry = await lockingHandleMediaFile.protect<bool>(() async {
|
||||
final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry();
|
||||
if (mediaFiles.isEmpty) {
|
||||
return checkForFailedUploads();
|
||||
}
|
||||
Log.info('re uploading ${mediaFiles.length} media files.');
|
||||
for (final mediaFile in mediaFiles) {
|
||||
if (mediaFile.messageIds == null || mediaFile.metadata == null) {
|
||||
if (appRestarted) {
|
||||
/// When the app got restarted and the messageIds or the metadata is not
|
||||
/// set then the app was closed before the images was send.
|
||||
await twonlyDB.mediaUploadsDao
|
||||
.deleteMediaUpload(mediaFile.mediaUploadId);
|
||||
Log.info(
|
||||
'upload can be removed, the finalized function was never called...',
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mediaFile.state == UploadState.readyToUpload) {
|
||||
await handleNextMediaUploadSteps(mediaFile.mediaUploadId);
|
||||
} else {
|
||||
await handlePreProcessingState(mediaFile);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (retry) {
|
||||
await retryMediaUpload(false, maxRetries: maxRetries - 1);
|
||||
}
|
||||
}
|
||||
|
||||
Future<MediaFileService?> initializeMediaUpload(
|
||||
MediaType type,
|
||||
|
|
@ -141,78 +38,10 @@ Future<MediaFileService?> initializeMediaUpload(
|
|||
return MediaFileService.fromMedia(mediaFile);
|
||||
}
|
||||
|
||||
Future<void> handlePreProcessingState(MediaUpload media) async {
|
||||
try {
|
||||
final imageHandler = readSendMediaFile(media.mediaUploadId, 'png');
|
||||
final videoHandler = compressVideoIfExists(media.mediaUploadId);
|
||||
await encryptMediaFiles(
|
||||
media.mediaUploadId,
|
||||
imageHandler,
|
||||
videoHandler,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('${media.mediaUploadId} got error in pre processing: $e');
|
||||
await handleUploadError(media);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> encryptMediaFiles(
|
||||
int mediaUploadId,
|
||||
Future<Uint8List> imageHandler,
|
||||
Future<bool>? videoHandler,
|
||||
) async {
|
||||
Log.info('$mediaUploadId encrypting files');
|
||||
var dataToEncrypt = await imageHandler;
|
||||
|
||||
/// if there is a video wait until it is finished with compression
|
||||
if (videoHandler != null) {
|
||||
if (await videoHandler) {
|
||||
final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4');
|
||||
dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo);
|
||||
}
|
||||
}
|
||||
|
||||
final state = MediaEncryptionData();
|
||||
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
|
||||
state
|
||||
..encryptionKey = secretKey.bytes
|
||||
..encryptionNonce = chacha20.newNonce();
|
||||
|
||||
final secretBox = await chacha20.encrypt(
|
||||
dataToEncrypt,
|
||||
secretKey: secretKey,
|
||||
nonce: state.encryptionNonce,
|
||||
);
|
||||
|
||||
state
|
||||
..encryptionMac = secretBox.mac.bytes
|
||||
..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
|
||||
|
||||
final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
|
||||
await writeSendMediaFile(
|
||||
mediaUploadId,
|
||||
'encrypted',
|
||||
encryptedBytes,
|
||||
);
|
||||
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
mediaUploadId,
|
||||
MediaUploadsCompanion(
|
||||
state: const Value(UploadState.readyToUpload),
|
||||
encryptionData: Value(state),
|
||||
),
|
||||
);
|
||||
unawaited(handleNextMediaUploadSteps(mediaUploadId));
|
||||
}
|
||||
|
||||
Future<void> finalizeUpload(
|
||||
Future<void> insertMediaFileInMessagesTable(
|
||||
MediaFileService mediaService,
|
||||
List<String> groupIds,
|
||||
) async {
|
||||
final messageIds = <Message>[];
|
||||
|
||||
for (final groupId in groupIds) {
|
||||
final message = await twonlyDB.messagesDao.insertMessage(
|
||||
MessagesCompanion(
|
||||
|
|
@ -221,7 +50,6 @@ Future<void> finalizeUpload(
|
|||
),
|
||||
);
|
||||
if (message != null) {
|
||||
messageIds.add(message);
|
||||
// de-archive contact when sending a new message
|
||||
await twonlyDB.groupsDao.updateGroup(
|
||||
message.groupId,
|
||||
|
|
@ -234,233 +62,131 @@ Future<void> finalizeUpload(
|
|||
}
|
||||
}
|
||||
|
||||
unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId));
|
||||
unawaited(startBackgroundMediaUpload(mediaService));
|
||||
}
|
||||
|
||||
final lockingHandleNextMediaUploadStep = Mutex();
|
||||
Future<void> handleNextMediaUploadSteps(String mediaUploadId) async {
|
||||
await lockingHandleNextMediaUploadStep.protect(() async {
|
||||
final mediaUpload = await twonlyDB.mediaUploadsDao
|
||||
.getMediaUploadById(mediaUploadId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (mediaUpload == null) return false;
|
||||
if (mediaUpload.state == UploadState.receiverNotified ||
|
||||
mediaUpload.state == UploadState.uploadTaskStarted) {
|
||||
/// Upload done and all users are notified :)
|
||||
Log.info('$mediaUploadId is already done');
|
||||
return false;
|
||||
Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
|
||||
if (mediaService.mediaFile.uploadState == UploadState.initialized) {
|
||||
await mediaService.setUploadState(UploadState.preprocessing);
|
||||
if (!mediaService.tempPath.existsSync()) {
|
||||
await mediaService.compressMedia();
|
||||
}
|
||||
try {
|
||||
/// Stage 1: media files are not yet encrypted...
|
||||
if (mediaUpload.encryptionData == null) {
|
||||
// when set this function will be called again by encryptAndPreUploadMediaFiles...
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mediaUpload.messageIds == null || mediaUpload.metadata == null) {
|
||||
/// the finalize function was not called yet...
|
||||
return false;
|
||||
}
|
||||
|
||||
await handleMediaUpload(mediaUpload);
|
||||
} catch (e) {
|
||||
Log.error('Non recoverable error while sending media file: $e');
|
||||
await handleUploadError(mediaUpload);
|
||||
if (!mediaService.encryptedPath.existsSync()) {
|
||||
await _encryptMediaFiles(mediaService);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!mediaService.uploadRequestPath.existsSync()) {
|
||||
await _createUploadRequest(mediaService);
|
||||
}
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
}
|
||||
|
||||
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
|
||||
await _uploadUploadRequest(mediaService);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// -- private functions --
|
||||
///
|
||||
///
|
||||
///
|
||||
Future<void> _encryptMediaFiles(MediaFileService mediaService) async {
|
||||
/// if there is a video wait until it is finished with compression
|
||||
|
||||
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
|
||||
var failed = false;
|
||||
final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', ''));
|
||||
final dataToEncrypt = await mediaService.tempPath.readAsBytes();
|
||||
|
||||
final media = await twonlyDB.mediaUploadsDao
|
||||
.getMediaUploadById(mediaUploadId)
|
||||
.getSingleOrNull();
|
||||
if (media == null) {
|
||||
Log.error(
|
||||
'Got an upload task but no upload media in the media upload database',
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (update.status == TaskStatus.failed ||
|
||||
update.status == TaskStatus.canceled) {
|
||||
Log.error('Upload failed: ${update.status}');
|
||||
failed = true;
|
||||
} else if (update.status == TaskStatus.complete) {
|
||||
if (update.responseStatusCode == 200) {
|
||||
await handleUploadSuccess(media);
|
||||
return;
|
||||
} else if (update.responseStatusCode != null) {
|
||||
if (update.responseStatusCode! >= 400 &&
|
||||
update.responseStatusCode! < 500) {
|
||||
failed = true;
|
||||
}
|
||||
Log.error(
|
||||
'Got error while uploading: ${update.responseStatusCode}',
|
||||
);
|
||||
}
|
||||
}
|
||||
final chacha20 = FlutterChacha20.poly1305Aead();
|
||||
|
||||
if (failed) {
|
||||
for (final messageId in media.messageIds!) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
messageId,
|
||||
const MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
errorWhileSending: Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Log.info(
|
||||
'Status update for ${update.task.taskId} with status ${update.status}',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleUploadSuccess(MediaUpload media) async {
|
||||
Log.info('Upload of ${media.mediaUploadId} success!');
|
||||
currentUploadTasks.remove(media.mediaUploadId);
|
||||
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
media.mediaUploadId,
|
||||
const MediaUploadsCompanion(
|
||||
state: Value(UploadState.receiverNotified),
|
||||
),
|
||||
final secretBox = await chacha20.encrypt(
|
||||
dataToEncrypt,
|
||||
secretKey: SecretKey(mediaService.mediaFile.encryptionKey!),
|
||||
nonce: mediaService.mediaFile.encryptionNonce,
|
||||
);
|
||||
|
||||
for (final messageId in media.messageIds!) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
messageId,
|
||||
const MessagesCompanion(
|
||||
acknowledgeByServer: Value(true),
|
||||
errorWhileSending: Value(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes));
|
||||
|
||||
mediaService.encryptedPath
|
||||
.writeAsBytesSync(Uint8List.fromList(secretBox.cipherText));
|
||||
|
||||
await mediaService.setUploadState(UploadState.uploading);
|
||||
}
|
||||
|
||||
Future<void> handleUploadError(MediaUpload mediaUpload) async {
|
||||
// if the messageIds are already there notify the user about this error...
|
||||
if (mediaUpload.messageIds != null) {
|
||||
for (final messageId in mediaUpload.messageIds!) {
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
messageId,
|
||||
const MessagesCompanion(
|
||||
errorWhileSending: Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId);
|
||||
}
|
||||
|
||||
Future<void> handleMediaUpload(MediaUpload media) async {
|
||||
final bytesToUpload =
|
||||
await readSendMediaFile(media.mediaUploadId, 'encrypted');
|
||||
|
||||
if (media.messageIds == null) return;
|
||||
|
||||
final messageIds = media.messageIds!;
|
||||
|
||||
Future<void> _createUploadRequest(MediaFileService media) async {
|
||||
final downloadTokens = <Uint8List>[];
|
||||
|
||||
final messagesOnSuccess = <TextMessage>[];
|
||||
|
||||
for (var i = 0; i < messageIds.length; i++) {
|
||||
final message = await twonlyDB.messagesDao
|
||||
.getMessageByMessageId(messageIds[i])
|
||||
.getSingleOrNull();
|
||||
if (message == null) continue;
|
||||
final messages =
|
||||
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId);
|
||||
|
||||
if (message.downloadState == DownloadState.downloaded) {
|
||||
// only upload message which are not yet uploaded (or in case of an error re-uploaded)
|
||||
continue;
|
||||
}
|
||||
for (final message in messages) {
|
||||
final groupMembers =
|
||||
await twonlyDB.groupsDao.getGroupMembers(message.groupId);
|
||||
for (final groupMember in groupMembers) {
|
||||
/// only send the upload to the users
|
||||
if (media.mediaFile.reuploadRequestedBy != null) {
|
||||
if (!media.mediaFile.reuploadRequestedBy!
|
||||
.contains(groupMember.contactId)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final downloadToken = getRandomUint8List(32);
|
||||
|
||||
final msg = MessageJson(
|
||||
kind: MessageKind.media,
|
||||
messageSenderId: messageIds[i],
|
||||
content: MediaMessageContent(
|
||||
downloadToken: downloadToken,
|
||||
maxShowTime: media.metadata!.maxShowTime,
|
||||
isRealTwonly: media.metadata!.isRealTwonly,
|
||||
isVideo: media.metadata!.isVideo,
|
||||
mirrorVideo: media.metadata!.mirrorVideo,
|
||||
encryptionKey: media.encryptionData!.encryptionKey,
|
||||
encryptionMac: media.encryptionData!.encryptionMac,
|
||||
encryptionNonce: media.encryptionData!.encryptionNonce,
|
||||
),
|
||||
timestamp: media.metadata!.messageSendAt,
|
||||
);
|
||||
|
||||
final plaintextContent = Uint8List.fromList(
|
||||
gzip.encode(utf8.encode(jsonEncode(msg.toJson()))),
|
||||
);
|
||||
|
||||
final contact = await twonlyDB.contactsDao
|
||||
.getContactByUserId(message.contactId)
|
||||
.getSingleOrNull();
|
||||
|
||||
if (contact == null || contact.deleted) {
|
||||
Log.warn(
|
||||
'Contact deleted ${message.contactId} or not found in database.',
|
||||
await twonlyDB.contactsDao.incFlameCounter(
|
||||
groupMember.contactId,
|
||||
false,
|
||||
message.createdAt,
|
||||
);
|
||||
await twonlyDB.messagesDao.updateMessageByMessageId(
|
||||
message.messageId,
|
||||
const MessagesCompanion(errorWhileSending: Value(true)),
|
||||
|
||||
final downloadToken = getRandomUint8List(32);
|
||||
|
||||
var type = EncryptedContent_Media_Type.IMAGE;
|
||||
if (media.mediaFile.type == MediaType.video) {
|
||||
type = EncryptedContent_Media_Type.VIDEO;
|
||||
} else if (media.mediaFile.type == MediaType.gif) {
|
||||
type = EncryptedContent_Media_Type.GIF;
|
||||
}
|
||||
|
||||
final notEncryptedContent = EncryptedContent(
|
||||
media: EncryptedContent_Media(
|
||||
senderMessageId: message.messageId,
|
||||
type: type,
|
||||
requiresAuthentication: media.mediaFile.requiresAuthentication,
|
||||
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
|
||||
downloadToken: media.mediaFile.downloadToken,
|
||||
encryptionKey: media.mediaFile.encryptionKey,
|
||||
encryptionNonce: media.mediaFile.encryptionNonce,
|
||||
encryptionMac: media.mediaFile.encryptionMac,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
|
||||
if (media.mediaFile.displayLimitInMilliseconds != null) {
|
||||
notEncryptedContent.media.displayLimitInMilliseconds =
|
||||
Int64(media.mediaFile.displayLimitInMilliseconds!);
|
||||
}
|
||||
|
||||
final cipherText = await sendCipherText(
|
||||
groupMember.contactId,
|
||||
notEncryptedContent,
|
||||
onlyReturnEncryptedData: true,
|
||||
);
|
||||
|
||||
if (cipherText == null) {
|
||||
Log.error(
|
||||
'Could not generate ciphertext message for ${groupMember.contactId}');
|
||||
}
|
||||
|
||||
final messageOnSuccess = TextMessage()
|
||||
..body = cipherText!.$1
|
||||
..userId = Int64(groupMember.contactId);
|
||||
|
||||
if (cipherText.$2 != null) {
|
||||
messageOnSuccess.pushData = cipherText.$2!;
|
||||
}
|
||||
|
||||
messagesOnSuccess.add(messageOnSuccess);
|
||||
downloadTokens.add(downloadToken);
|
||||
}
|
||||
|
||||
await twonlyDB.contactsDao.incFlameCounter(
|
||||
message.contactId,
|
||||
false,
|
||||
message.sendAt,
|
||||
);
|
||||
|
||||
final encryptedBytes = await signalEncryptMessage(
|
||||
message.contactId,
|
||||
plaintextContent,
|
||||
);
|
||||
|
||||
if (encryptedBytes == null) continue;
|
||||
|
||||
final messageOnSuccess = TextMessage()
|
||||
..body = encryptedBytes
|
||||
..userId = Int64(message.contactId);
|
||||
|
||||
final pushKind = (media.metadata!.isRealTwonly)
|
||||
? PushKind.twonly
|
||||
: (media.metadata!.isVideo)
|
||||
? PushKind.video
|
||||
: PushKind.image;
|
||||
|
||||
final pushData = await getPushData(
|
||||
message.contactId,
|
||||
PushNotification(
|
||||
messageId: Int64(message.messageId),
|
||||
kind: pushKind,
|
||||
),
|
||||
);
|
||||
if (pushData != null) {
|
||||
messageOnSuccess.pushData = pushData.toList();
|
||||
}
|
||||
|
||||
messagesOnSuccess.add(messageOnSuccess);
|
||||
downloadTokens.add(downloadToken);
|
||||
}
|
||||
|
||||
final bytesToUpload = await media.encryptedPath.readAsBytes();
|
||||
|
||||
final uploadRequest = UploadRequest(
|
||||
messagesOnSuccess: messagesOnSuccess,
|
||||
downloadTokens: downloadTokens,
|
||||
|
|
@ -469,6 +195,10 @@ Future<void> handleMediaUpload(MediaUpload media) async {
|
|||
|
||||
final uploadRequestBytes = uploadRequest.writeToBuffer();
|
||||
|
||||
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
|
||||
}
|
||||
|
||||
Future<void> _uploadUploadRequest(MediaFileService media) async {
|
||||
final apiAuthTokenRaw = await const FlutterSecureStorage()
|
||||
.read(key: SecureStorageKeys.apiAuthToken);
|
||||
if (apiAuthTokenRaw == null) {
|
||||
|
|
@ -477,108 +207,27 @@ Future<void> handleMediaUpload(MediaUpload media) async {
|
|||
}
|
||||
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
|
||||
|
||||
final uploadRequestFile = await writeSendMediaFile(
|
||||
media.mediaUploadId,
|
||||
'upload',
|
||||
uploadRequestBytes,
|
||||
);
|
||||
|
||||
final apiUrl =
|
||||
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
|
||||
|
||||
try {
|
||||
Log.info('Starting upload from ${media.mediaUploadId}');
|
||||
// try {
|
||||
Log.info('Starting upload from ${media.mediaFile.mediaId}');
|
||||
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: 'upload_${media.mediaUploadId}',
|
||||
displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video',
|
||||
file: uploadRequestFile,
|
||||
url: apiUrl,
|
||||
priority: 0,
|
||||
retries: 10,
|
||||
headers: {
|
||||
'x-twonly-auth-token': apiAuthToken,
|
||||
},
|
||||
);
|
||||
|
||||
currentUploadTasks[media.mediaUploadId] = task;
|
||||
|
||||
try {
|
||||
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
|
||||
} catch (e) {
|
||||
Log.error('Fast upload failed: $e. Using slow method directly.');
|
||||
await enqueueUploadTask(media.mediaUploadId);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.error('Exception during upload: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Map<int, UploadTask> currentUploadTasks = {};
|
||||
|
||||
Future<void> enqueueUploadTask(int mediaUploadId) async {
|
||||
if (currentUploadTasks[mediaUploadId] == null) {
|
||||
Log.info('could not enqueue upload task: $mediaUploadId');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info('Enqueue upload task: $mediaUploadId');
|
||||
|
||||
await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!);
|
||||
currentUploadTasks.remove(mediaUploadId);
|
||||
|
||||
await twonlyDB.mediaUploadsDao.updateMediaUpload(
|
||||
mediaUploadId,
|
||||
const MediaUploadsCompanion(
|
||||
state: Value(UploadState.uploadTaskStarted),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleUploadWhenAppGoesBackground() async {
|
||||
if (currentUploadTasks.keys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
Log.info('App goes into background. Enqueue uploads to the background.');
|
||||
final keys = currentUploadTasks.keys.toList();
|
||||
for (final key in keys) {
|
||||
await enqueueUploadTask(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> uploadFileFast(
|
||||
MediaUpload media,
|
||||
Uint8List uploadRequestFile,
|
||||
String apiUrl,
|
||||
String apiAuthToken,
|
||||
) async {
|
||||
final requestMultipart = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse(apiUrl),
|
||||
);
|
||||
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
|
||||
|
||||
requestMultipart.files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
uploadRequestFile,
|
||||
filename: 'upload',
|
||||
),
|
||||
final task = UploadTask.fromFile(
|
||||
taskId: 'upload_${media.mediaFile.mediaId}',
|
||||
displayName: media.mediaFile.type.name,
|
||||
file: media.uploadRequestPath,
|
||||
url: apiUrl,
|
||||
priority: 0,
|
||||
retries: 10,
|
||||
headers: {
|
||||
'x-twonly-auth-token': apiAuthToken,
|
||||
},
|
||||
);
|
||||
|
||||
final response = await requestMultipart.send();
|
||||
if (response.statusCode == 200) {
|
||||
Log.info('Upload successful!');
|
||||
await handleUploadSuccess(media);
|
||||
return;
|
||||
} else if (response.statusCode == 429) {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
media.mediaId,
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.uploadLimitReached),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Log.info('Upload failed with status: ${response.statusCode}');
|
||||
}
|
||||
Log.info('Enqueue upload task: ${task.taskId}');
|
||||
|
||||
await FileDownloader().enqueue(task);
|
||||
|
||||
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,18 +30,20 @@ Future<void> tryTransmitMessages() async {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> tryToSendCompleteMessage({
|
||||
// When the ackByServerAt is set this value is written in the receipted
|
||||
Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
|
||||
String? receiptId,
|
||||
Receipt? receipt,
|
||||
bool reupload = false,
|
||||
bool onlyReturnEncryptedData = false,
|
||||
}) async {
|
||||
try {
|
||||
if (receiptId == null && receipt == null) return;
|
||||
if (receiptId == null && receipt == null) return null;
|
||||
if (receipt == null) {
|
||||
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
|
||||
if (receipt == null) {
|
||||
Log.error('Receipt $receiptId not found.');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
receiptId = receipt.receiptId;
|
||||
|
|
@ -55,9 +57,9 @@ Future<void> tryToSendCompleteMessage({
|
|||
);
|
||||
}
|
||||
|
||||
if (receipt.ackByServerAt != null) {
|
||||
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
|
||||
Log.error('$receiptId message already uploaded!');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.info('Uploading $receiptId (Message to ${receipt.contactId})');
|
||||
|
|
@ -86,7 +88,7 @@ Future<void> tryToSendCompleteMessage({
|
|||
);
|
||||
if (cipherText == null) {
|
||||
Log.error('Could not encrypt the message. Aborting and trying again.');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
message.encryptedContent = cipherText.serialize();
|
||||
switch (cipherText.getType()) {
|
||||
|
|
@ -96,10 +98,14 @@ Future<void> tryToSendCompleteMessage({
|
|||
message.type = pb.Message_Type.CIPHERTEXT;
|
||||
default:
|
||||
Log.error('Invalid ciphertext type: ${cipherText.getType()}.');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (onlyReturnEncryptedData) {
|
||||
return (message.writeToBuffer(), pushData);
|
||||
}
|
||||
|
||||
final resp = await apiService.sendTextMessage(
|
||||
receipt.contactId,
|
||||
message.writeToBuffer(),
|
||||
|
|
@ -114,7 +120,7 @@ Future<void> tryToSendCompleteMessage({
|
|||
receipt.contactId,
|
||||
const ContactsCompanion(deleted: Value(true)),
|
||||
);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,12 +155,14 @@ Future<void> tryToSendCompleteMessage({
|
|||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> sendCipherText(
|
||||
Future<(Uint8List, Uint8List?)?> sendCipherText(
|
||||
int contactId,
|
||||
pb.EncryptedContent encryptedContent,
|
||||
) async {
|
||||
pb.EncryptedContent encryptedContent, {
|
||||
bool onlyReturnEncryptedData = false,
|
||||
}) async {
|
||||
final response = pb.Message()
|
||||
..type = pb.Message_Type.CIPHERTEXT
|
||||
..encryptedContent = encryptedContent.writeToBuffer();
|
||||
|
|
@ -163,12 +171,17 @@ Future<void> sendCipherText(
|
|||
ReceiptsCompanion(
|
||||
contactId: Value(contactId),
|
||||
message: Value(response.writeToBuffer()),
|
||||
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null),
|
||||
),
|
||||
);
|
||||
|
||||
if (receipt != null) {
|
||||
await tryToSendCompleteMessage(receipt: receipt);
|
||||
return tryToSendCompleteMessage(
|
||||
receipt: receipt,
|
||||
onlyReturnEncryptedData: onlyReturnEncryptedData,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> notifyContactAboutOpeningMessage(
|
||||
|
|
|
|||
|
|
@ -49,6 +49,26 @@ class MediaFileService {
|
|||
await updateFromDB();
|
||||
}
|
||||
|
||||
Future<void> setUploadState(UploadState uploadState) async {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
uploadState: Value(uploadState),
|
||||
),
|
||||
);
|
||||
await updateFromDB();
|
||||
}
|
||||
|
||||
Future<void> setEncryptedMac(Uint8List encryptionMac) async {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
MediaFilesCompanion(
|
||||
encryptionMac: Value(encryptionMac),
|
||||
),
|
||||
);
|
||||
await updateFromDB();
|
||||
}
|
||||
|
||||
Future<void> setRequiresAuth(bool requiresAuthentication) async {
|
||||
await twonlyDB.mediaFilesDao.updateMedia(
|
||||
mediaFile.mediaId,
|
||||
|
|
@ -98,7 +118,8 @@ class MediaFileService {
|
|||
encryptedPath,
|
||||
originalPath,
|
||||
storedPath,
|
||||
thumbnailPath
|
||||
thumbnailPath,
|
||||
uploadRequestPath
|
||||
];
|
||||
|
||||
for (final path in pathsToRemove) {
|
||||
|
|
@ -160,6 +181,10 @@ class MediaFileService {
|
|||
'tmp',
|
||||
namePrefix: '.encrypted',
|
||||
);
|
||||
File get uploadRequestPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.upload',
|
||||
);
|
||||
File get originalPath => _buildFilePath(
|
||||
'tmp',
|
||||
namePrefix: '.original',
|
||||
|
|
|
|||
|
|
@ -8,12 +8,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:twonly/src/constants/secure_storage_keys.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/push_notification.pbenum.dart';
|
||||
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/views/camera/share_image_editor_view.dart'
|
||||
show gMediaShowInfinite;
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
|
@ -34,7 +31,7 @@ Future<void> customLocalPushNotification(String title, String msg) async {
|
|||
);
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
gMediaShowInfinite + Random.secure().nextInt(9999),
|
||||
Random.secure().nextInt(9999),
|
||||
title,
|
||||
msg,
|
||||
notificationDetails,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import 'package:drift/drift.dart';
|
|||
import 'package:hashlib/hashlib.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
Future<void> enableTwonlySafe(String password) async {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import 'package:twonly/src/constants/secure_storage_keys.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/model/json/userdata.dart';
|
||||
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/twonly_safe/common.twonly_safe.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
import 'package:twonly/src/views/settings/backup/backup.view.dart';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,22 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
||||
class SaveToGalleryButton extends StatefulWidget {
|
||||
const SaveToGalleryButton({
|
||||
required this.getMergedImage,
|
||||
required this.isLoading,
|
||||
required this.displayButtonLabel,
|
||||
required this.mediaService,
|
||||
super.key,
|
||||
this.mediaUploadId,
|
||||
this.videoFilePath,
|
||||
});
|
||||
final Future<Uint8List?> Function() getMergedImage;
|
||||
final bool displayButtonLabel;
|
||||
final File? videoFilePath;
|
||||
final int? mediaUploadId;
|
||||
final MediaFileService mediaService;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
|
|
@ -54,44 +46,20 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
|
|||
});
|
||||
|
||||
String? res;
|
||||
var memoryPath = await getMediaBaseFilePath('memories');
|
||||
|
||||
if (widget.mediaUploadId != null) {
|
||||
memoryPath = join(memoryPath, '${widget.mediaUploadId!}');
|
||||
} else {
|
||||
final random = Random();
|
||||
final token = uint8ListToHex(
|
||||
List<int>.generate(32, (i) => random.nextInt(256)),
|
||||
);
|
||||
memoryPath = join(memoryPath, token);
|
||||
}
|
||||
final user = await getUser();
|
||||
if (user == null) return;
|
||||
final storeToGallery = user.storeMediaFilesInGallery;
|
||||
final storedMediaPath = widget.mediaService.storedPath;
|
||||
|
||||
if (widget.videoFilePath != null) {
|
||||
memoryPath += '.mp4';
|
||||
await File(widget.videoFilePath!.path).copy(memoryPath);
|
||||
unawaited(createThumbnailsForVideo(File(memoryPath)));
|
||||
if (storeToGallery) {
|
||||
res = await saveVideoToGallery(widget.videoFilePath!.path);
|
||||
}
|
||||
} else {
|
||||
final imageBytes = await widget.getMergedImage();
|
||||
if (imageBytes == null || !mounted) return;
|
||||
final webPImageBytes =
|
||||
await FlutterImageCompress.compressWithList(
|
||||
format: CompressFormat.webp,
|
||||
imageBytes,
|
||||
quality: 100,
|
||||
);
|
||||
memoryPath += '.png';
|
||||
await File(memoryPath).writeAsBytes(webPImageBytes);
|
||||
unawaited(createThumbnailsForImage(File(memoryPath)));
|
||||
if (storeToGallery) {
|
||||
res = await saveImageToGallery(imageBytes);
|
||||
}
|
||||
final storeToGallery = gUser.storeMediaFilesInGallery;
|
||||
|
||||
await widget.mediaService.storeMediaFile();
|
||||
|
||||
if (storeToGallery) {
|
||||
res = await saveVideoToGallery(storedMediaPath.path);
|
||||
}
|
||||
|
||||
await widget.mediaService.compressMedia();
|
||||
await widget.mediaService.createThumbnail();
|
||||
|
||||
if (res == null) {
|
||||
setState(() {
|
||||
_imageSaved = true;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -9,7 +8,6 @@ import 'package:image_picker/image_picker.dart';
|
|||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
|
|
@ -92,9 +90,9 @@ class CameraPreviewControllerView extends StatelessWidget {
|
|||
required this.selectedCameraDetails,
|
||||
required this.screenshotController,
|
||||
super.key,
|
||||
this.sendTo,
|
||||
this.sendToGroup,
|
||||
});
|
||||
final Contact? sendTo;
|
||||
final Group? sendToGroup;
|
||||
final Future<CameraController?> Function(
|
||||
int sCameraId,
|
||||
bool init,
|
||||
|
|
@ -112,7 +110,7 @@ class CameraPreviewControllerView extends StatelessWidget {
|
|||
if (snap.hasData) {
|
||||
if (snap.data!) {
|
||||
return CameraPreviewView(
|
||||
sendTo: sendTo,
|
||||
sendToGroup: sendToGroup,
|
||||
selectCamera: selectCamera,
|
||||
cameraController: cameraController,
|
||||
selectedCameraDetails: selectedCameraDetails,
|
||||
|
|
@ -141,9 +139,9 @@ class CameraPreviewView extends StatefulWidget {
|
|||
required this.selectedCameraDetails,
|
||||
required this.screenshotController,
|
||||
super.key,
|
||||
this.sendTo,
|
||||
this.sendToGroup,
|
||||
});
|
||||
final Contact? sendTo;
|
||||
final Group? sendToGroup;
|
||||
final Future<CameraController?> Function(
|
||||
int sCameraId,
|
||||
bool init,
|
||||
|
|
@ -328,7 +326,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
pageBuilder: (context, a1, a2) => ShareImageEditorView(
|
||||
imageBytesFuture: imageBytes,
|
||||
sharedFromGallery: sharedFromGallery,
|
||||
sendTo: widget.sendTo,
|
||||
sendToGroup: widget.sendToGroup,
|
||||
mediaFileService: mediaFileService,
|
||||
),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
|
|
@ -347,7 +345,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
if (!mounted) return true;
|
||||
// shouldReturn is null when the user used the back button
|
||||
if (shouldReturn != null && shouldReturn) {
|
||||
if (widget.sendTo == null) {
|
||||
if (widget.sendToGroup == null) {
|
||||
globalUpdateOfHomeViewPageIndex(0);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
|
|
@ -476,19 +474,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
});
|
||||
|
||||
try {
|
||||
File? videoPathFile;
|
||||
final videoPath = await widget.cameraController?.stopVideoRecording();
|
||||
if (videoPath != null) {
|
||||
if (Platform.isAndroid) {
|
||||
// see https://github.com/flutter/flutter/issues/148335
|
||||
await File(videoPath.path).rename('${videoPath.path}.mp4');
|
||||
videoPathFile = File('${videoPath.path}.mp4');
|
||||
} else {
|
||||
videoPathFile = File(videoPath.path);
|
||||
}
|
||||
}
|
||||
if (videoPath == null) return;
|
||||
await widget.cameraController?.pausePreview();
|
||||
if (await pushMediaEditor(null, videoPathFile)) {
|
||||
if (await pushMediaEditor(null, File(videoPath.path))) {
|
||||
return;
|
||||
}
|
||||
} on CameraException catch (e) {
|
||||
|
|
@ -568,9 +557,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
),
|
||||
),
|
||||
if (!sharePreviewIsShown &&
|
||||
widget.sendTo != null &&
|
||||
widget.sendToGroup != null &&
|
||||
!isVideoRecording)
|
||||
SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)),
|
||||
SendToWidget(sendTo: widget.sendToGroup!.groupName),
|
||||
if (!sharePreviewIsShown && !isVideoRecording)
|
||||
Positioned(
|
||||
right: 5,
|
||||
|
|
@ -722,7 +711,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
|||
videoRecordingStarted: videoRecordingStarted,
|
||||
maxVideoRecordingTime: maxVideoRecordingTime,
|
||||
),
|
||||
if (!sharePreviewIsShown && widget.sendTo != null)
|
||||
if (!sharePreviewIsShown && widget.sendToGroup != null)
|
||||
Positioned(
|
||||
left: 5,
|
||||
top: 10,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import 'package:twonly/src/views/camera/camera_preview_components/camera_preview
|
|||
import 'package:twonly/src/views/camera/camera_preview_controller_view.dart';
|
||||
|
||||
class CameraSendToView extends StatefulWidget {
|
||||
const CameraSendToView(this.sendTo, {super.key});
|
||||
final Contact sendTo;
|
||||
const CameraSendToView(this.sendToGroup, {super.key});
|
||||
final Group sendToGroup;
|
||||
@override
|
||||
State<CameraSendToView> createState() => CameraSendToViewState();
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
|
|||
),
|
||||
CameraPreviewControllerView(
|
||||
selectCamera: selectCamera,
|
||||
sendTo: widget.sendTo,
|
||||
sendToGroup: widget.sendToGroup,
|
||||
cameraController: cameraController,
|
||||
selectedCameraDetails: selectedCameraDetails,
|
||||
screenshotController: screenshotController,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,11 @@
|
|||
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:hashlib/random.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
|
|
@ -28,25 +23,22 @@ import 'package:twonly/src/views/camera/image_editor/modules/all_emojis.dart';
|
|||
import 'package:twonly/src/views/camera/share_image_view.dart';
|
||||
import 'package:twonly/src/views/components/media_view_sizing.dart';
|
||||
import 'package:twonly/src/views/components/notification_badge.dart';
|
||||
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
List<Layer> layers = [];
|
||||
List<Layer> undoLayers = [];
|
||||
List<Layer> removedLayers = [];
|
||||
|
||||
const gMediaShowInfinite = 999999;
|
||||
|
||||
class ShareImageEditorView extends StatefulWidget {
|
||||
const ShareImageEditorView({
|
||||
required this.sharedFromGallery,
|
||||
required this.mediaFileService,
|
||||
super.key,
|
||||
this.imageBytesFuture,
|
||||
this.sendTo,
|
||||
this.sendToGroup,
|
||||
});
|
||||
final Future<Uint8List?>? imageBytesFuture;
|
||||
final Group? sendTo;
|
||||
final Group? sendToGroup;
|
||||
final bool sharedFromGallery;
|
||||
final MediaFileService mediaFileService;
|
||||
@override
|
||||
|
|
@ -66,9 +58,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
ImageItem currentImage = ImageItem();
|
||||
ScreenshotController screenshotController = ScreenshotController();
|
||||
|
||||
/// Media upload variables
|
||||
Future<bool>? videoUploadHandler;
|
||||
|
||||
MediaFileService get mediaService => widget.mediaFileService;
|
||||
MediaFile get media => widget.mediaFileService.mediaFile;
|
||||
|
||||
|
|
@ -78,8 +67,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
|
||||
layers.add(FilterLayerData());
|
||||
|
||||
if (widget.sendTo != null) {
|
||||
selectedGroupIds.add(widget.sendTo!.groupId);
|
||||
if (widget.sendToGroup != null) {
|
||||
selectedGroupIds.add(widget.sendToGroup!.groupId);
|
||||
}
|
||||
|
||||
if (widget.imageBytesFuture != null) {
|
||||
|
|
@ -284,17 +273,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
|
||||
Future<void> pushShareImageView() async {
|
||||
final imageBytes = storeImageAsOriginal();
|
||||
final mediaStoreFuture =
|
||||
(media.type == MediaType.image) ? storeImageAsOriginal() : null;
|
||||
|
||||
await videoController?.pause();
|
||||
if (isDisposed || !mounted) return;
|
||||
final wasSend = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ShareImageView(
|
||||
imageBytesFuture: imageBytes,
|
||||
selectedUserIds: selectedGroupIds,
|
||||
updateStatus: updateSelectedGroupIds,
|
||||
videoUploadHandler: videoUploadHandler,
|
||||
selectedGroupIds: selectedGroupIds,
|
||||
updateSelectedGroupIds: updateSelectedGroupIds,
|
||||
mediaStoreFuture: mediaStoreFuture,
|
||||
mediaFileService: mediaService,
|
||||
),
|
||||
),
|
||||
|
|
@ -306,11 +296,11 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> storeImageAsOriginal() async {
|
||||
Future<Uint8List?> getEditedImageBytes() async {
|
||||
if (layers.length == 1) {
|
||||
if (layers.first is BackgroundLayerData) {
|
||||
final image = (layers.first as BackgroundLayerData).image.bytes;
|
||||
mediaService.originalPath.writeAsBytesSync(image);
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -324,24 +314,31 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
);
|
||||
if (image == null) {
|
||||
Log.error('screenshotController did not return image bytes');
|
||||
return;
|
||||
}
|
||||
|
||||
mediaService.originalPath.writeAsBytesSync(image);
|
||||
|
||||
// In case the image was already stored, then rename the stored image.
|
||||
|
||||
if (mediaService.storedPath.existsSync()) {
|
||||
final newPath = mediaService.storedPath.absolute.path
|
||||
.replaceFirst(media.mediaId, uuid.v7());
|
||||
mediaService.storedPath.renameSync(newPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final x in layers) {
|
||||
x.showCustomButtons = true;
|
||||
}
|
||||
setState(() {});
|
||||
return image;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> storeImageAsOriginal() async {
|
||||
final imageBytes = await getEditedImageBytes();
|
||||
if (imageBytes == null) return false;
|
||||
mediaService.originalPath.writeAsBytesSync(imageBytes);
|
||||
|
||||
// In case the image was already stored, then rename the stored image.
|
||||
if (mediaService.storedPath.existsSync()) {
|
||||
final newPath = mediaService.storedPath.absolute.path
|
||||
.replaceFirst(media.mediaId, uuid.v7());
|
||||
mediaService.storedPath.renameSync(newPath);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
|
||||
|
|
@ -377,14 +374,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
|
||||
if (!context.mounted) return;
|
||||
|
||||
// first finalize the upload
|
||||
await finalizeUpload(mediaService, [widget.sendTo!.groupId]);
|
||||
|
||||
/// then call the upload process in the background
|
||||
await encryptMediaFiles(
|
||||
mediaUploadId!,
|
||||
imageHandler,
|
||||
videoUploadHandler,
|
||||
// Insert media file into the messages database and start uploading process in the background
|
||||
await insertMediaFileInMessagesTable(
|
||||
mediaService,
|
||||
[widget.sendToGroup!.groupId],
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
|
|
@ -434,14 +427,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SaveToGalleryButton(
|
||||
getMergedImage: getMergedImage,
|
||||
mediaUploadId: mediaUploadId,
|
||||
videoFilePath: widget.videoFilePath,
|
||||
displayButtonLabel: widget.sendTo == null,
|
||||
getMergedImage: getEditedImageBytes,
|
||||
mediaService: mediaService,
|
||||
displayButtonLabel: widget.sendToGroup == null,
|
||||
isLoading: loadingImage,
|
||||
),
|
||||
if (widget.sendTo != null) const SizedBox(width: 10),
|
||||
if (widget.sendTo != null)
|
||||
if (widget.sendToGroup != null) const SizedBox(width: 10),
|
||||
if (widget.sendToGroup != null)
|
||||
OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
|
|
@ -451,7 +443,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
onPressed: pushShareImageView,
|
||||
child: const FaIcon(FontAwesomeIcons.userPlus),
|
||||
),
|
||||
SizedBox(width: widget.sendTo == null ? 20 : 10),
|
||||
SizedBox(width: widget.sendToGroup == null ? 20 : 10),
|
||||
FilledButton.icon(
|
||||
icon: sendingOrLoadingImage
|
||||
? SizedBox(
|
||||
|
|
@ -467,7 +459,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
: const FaIcon(FontAwesomeIcons.solidPaperPlane),
|
||||
onPressed: () async {
|
||||
if (sendingOrLoadingImage) return;
|
||||
if (widget.sendTo == null) return pushShareImageView();
|
||||
if (widget.sendToGroup == null)
|
||||
return pushShareImageView();
|
||||
await sendImageToSinglePerson();
|
||||
},
|
||||
style: ButtonStyle(
|
||||
|
|
@ -479,9 +472,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
),
|
||||
),
|
||||
label: Text(
|
||||
(widget.sendTo == null)
|
||||
(widget.sendToGroup == null)
|
||||
? context.lang.shareImagedEditorShareWith
|
||||
: getContactDisplayName(widget.sendTo!),
|
||||
: widget.sendToGroup!.groupName,
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/database/daos/contacts.dao.dart';
|
||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||
|
|
@ -21,19 +22,15 @@ import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
|
|||
|
||||
class ShareImageView extends StatefulWidget {
|
||||
const ShareImageView({
|
||||
required this.imageBytesFuture,
|
||||
required this.selectedUserIds,
|
||||
required this.updateStatus,
|
||||
required this.videoUploadHandler,
|
||||
required this.selectedGroupIds,
|
||||
required this.updateSelectedGroupIds,
|
||||
required this.mediaStoreFuture,
|
||||
required this.mediaFileService,
|
||||
super.key,
|
||||
this.enableVideoAudio,
|
||||
});
|
||||
final Future<Uint8List?> imageBytesFuture;
|
||||
final HashSet<int> selectedUserIds;
|
||||
final bool? enableVideoAudio;
|
||||
final void Function(int, bool) updateStatus;
|
||||
final Future<bool>? videoUploadHandler;
|
||||
final HashSet<String> selectedGroupIds;
|
||||
final void Function(String, bool) updateSelectedGroupIds;
|
||||
final Future<bool>? mediaStoreFuture;
|
||||
final MediaFileService mediaFileService;
|
||||
|
||||
@override
|
||||
|
|
@ -69,17 +66,11 @@ class _ShareImageView extends State<ShareImageView> {
|
|||
}
|
||||
|
||||
Future<void> initAsync() async {
|
||||
imageBytes = await widget.imageBytesFuture;
|
||||
if (imageBytes != null) {
|
||||
final imageHandler =
|
||||
addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!);
|
||||
// start with the pre upload of the media file...
|
||||
await encryptMediaFiles(
|
||||
widget.mediaUploadId,
|
||||
imageHandler,
|
||||
widget.videoUploadHandler,
|
||||
);
|
||||
if (widget.mediaStoreFuture != null) {
|
||||
await widget.mediaStoreFuture;
|
||||
}
|
||||
await widget.mediaFileService.setUploadState(UploadState.preprocessing);
|
||||
unawaited(startBackgroundMediaUpload(widget.mediaFileService));
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue