diff --git a/lib/main.dart b/lib/main.dart index 431626c..eb2fdb4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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'; diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index b004215..3747ab4 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -26,4 +26,9 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { await (update(groups)..where((c) => c.groupId.equals(groupId))) .write(updates); } + + Future> getGroupMembers(String groupId) async { + return (select(groupMembers)..where((t) => t.groupId.equals(groupId))) + .get(); + } } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index 07fd0a6..0483dd8 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -285,6 +285,14 @@ class MessagesDao extends DatabaseAccessor with _$MessagesDaoMixin { .write(updatedValues); } + Future updateMessagesByMediaId( + String mediaId, + MessagesCompanion updatedValues, + ) { + return (update(messages)..where((c) => c.mediaId.equals(mediaId))) + .write(updatedValues); + } + Future insertMessage(MessagesCompanion message) async { try { final rowId = await into(messages).insert(message); diff --git a/lib/src/database/tables/groups.table.dart b/lib/src/database/tables/groups.table.dart index e6548e4..27b6438 100644 --- a/lib/src/database/tables/groups.table.dart +++ b/lib/src/database/tables/groups.table.dart @@ -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)(); diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 8818f0a..26b9a8d 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -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, diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index 08c28f0..28438c8 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -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 => diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index f3292b9..118a6d5 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -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 groupName = GeneratedColumn( + '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 { 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 { 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 { map['is_group_of_two'] = Variable(isGroupOfTwo); map['pinned'] = Variable(pinned); map['archived'] = Variable(archived); + map['group_name'] = Variable(groupName); map['last_message_exchange'] = Variable(lastMessageExchange); map['created_at'] = Variable(createdAt); return map; @@ -1204,6 +1222,7 @@ class Group extends DataClass implements Insertable { 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 { isGroupOfTwo: serializer.fromJson(json['isGroupOfTwo']), pinned: serializer.fromJson(json['pinned']), archived: serializer.fromJson(json['archived']), + groupName: serializer.fromJson(json['groupName']), lastMessageExchange: serializer.fromJson(json['lastMessageExchange']), createdAt: serializer.fromJson(json['createdAt']), @@ -1232,6 +1252,7 @@ class Group extends DataClass implements Insertable { 'isGroupOfTwo': serializer.toJson(isGroupOfTwo), 'pinned': serializer.toJson(pinned), 'archived': serializer.toJson(archived), + 'groupName': serializer.toJson(groupName), 'lastMessageExchange': serializer.toJson(lastMessageExchange), 'createdAt': serializer.toJson(createdAt), }; @@ -1243,6 +1264,7 @@ class Group extends DataClass implements Insertable { bool? isGroupOfTwo, bool? pinned, bool? archived, + String? groupName, DateTime? lastMessageExchange, DateTime? createdAt}) => Group( @@ -1251,6 +1273,7 @@ class Group extends DataClass implements Insertable { 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 { : 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 { ..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 { @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 { 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 { final Value isGroupOfTwo; final Value pinned; final Value archived; + final Value groupName; final Value lastMessageExchange; final Value createdAt; final Value rowid; @@ -1317,6 +1344,7 @@ class GroupsCompanion extends UpdateCompanion { 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 { 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 custom({ Expression? groupId, Expression? isGroupAdmin, Expression? isGroupOfTwo, Expression? pinned, Expression? archived, + Expression? groupName, Expression? lastMessageExchange, Expression? createdAt, Expression? rowid, @@ -1348,6 +1379,7 @@ class GroupsCompanion extends UpdateCompanion { 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 { Value? isGroupOfTwo, Value? pinned, Value? archived, + Value? groupName, Value? lastMessageExchange, Value? createdAt, Value? rowid}) { @@ -1370,6 +1403,7 @@ class GroupsCompanion extends UpdateCompanion { 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 { if (archived.present) { map['archived'] = Variable(archived.value); } + if (groupName.present) { + map['group_name'] = Variable(groupName.value); + } if (lastMessageExchange.present) { map['last_message_exchange'] = Variable(lastMessageExchange.value); @@ -1415,6 +1452,7 @@ class GroupsCompanion extends UpdateCompanion { ..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 mediaStored = GeneratedColumn( + '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 { 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 { 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 { if (!nullToAbsent || mediaId != null) { map['media_id'] = Variable(mediaId); } + map['media_stored'] = Variable(mediaStored); if (!nullToAbsent || downloadToken != null) { map['download_token'] = Variable(downloadToken); } @@ -2548,6 +2608,7 @@ class Message extends DataClass implements Insertable { 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 { senderId: serializer.fromJson(json['senderId']), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), + mediaStored: serializer.fromJson(json['mediaStored']), downloadToken: serializer.fromJson(json['downloadToken']), quotesMessageId: serializer.fromJson(json['quotesMessageId']), isDeletedFromSender: @@ -2600,6 +2662,7 @@ class Message extends DataClass implements Insertable { 'senderId': serializer.toJson(senderId), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), + 'mediaStored': serializer.toJson(mediaStored), 'downloadToken': serializer.toJson(downloadToken), 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), @@ -2619,6 +2682,7 @@ class Message extends DataClass implements Insertable { Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + bool? mediaStored, Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, @@ -2635,6 +2699,7 @@ class Message extends DataClass implements Insertable { 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 { 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 { ..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 { senderId, content, mediaId, + mediaStored, $driftBlobEquality.hash(downloadToken), quotesMessageId, isDeletedFromSender, @@ -2727,6 +2796,7 @@ class Message extends DataClass implements Insertable { 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 { final Value senderId; final Value content; final Value mediaId; + final Value mediaStored; final Value downloadToken; final Value quotesMessageId; final Value isDeletedFromSender; @@ -2762,6 +2833,7 @@ class MessagesCompanion extends UpdateCompanion { 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 { 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 { Expression? senderId, Expression? content, Expression? mediaId, + Expression? mediaStored, Expression? downloadToken, Expression? quotesMessageId, Expression? isDeletedFromSender, @@ -2816,6 +2890,7 @@ class MessagesCompanion extends UpdateCompanion { 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 { Value? senderId, Value? content, Value? mediaId, + Value? mediaStored, Value? downloadToken, Value? quotesMessageId, Value? isDeletedFromSender, @@ -2854,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion { 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 { if (mediaId.present) { map['media_id'] = Variable(mediaId.value); } + if (mediaStored.present) { + map['media_stored'] = Variable(mediaStored.value); + } if (downloadToken.present) { map['download_token'] = Variable(downloadToken.value); } @@ -2930,6 +3010,7 @@ class MessagesCompanion extends UpdateCompanion { ..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 pinned, Value archived, + required String groupName, Value lastMessageExchange, Value createdAt, Value rowid, @@ -6873,6 +6955,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({ Value isGroupOfTwo, Value pinned, Value archived, + Value groupName, Value lastMessageExchange, Value createdAt, Value rowid, @@ -6921,6 +7004,9 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnFilters get archived => $composableBuilder( column: $table.archived, builder: (column) => ColumnFilters(column)); + ColumnFilters get groupName => $composableBuilder( + column: $table.groupName, builder: (column) => ColumnFilters(column)); + ColumnFilters get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnFilters(column)); @@ -6975,6 +7061,9 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> { ColumnOrderings get archived => $composableBuilder( column: $table.archived, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get groupName => $composableBuilder( + column: $table.groupName, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => ColumnOrderings(column)); @@ -7007,6 +7096,9 @@ class $$GroupsTableAnnotationComposer GeneratedColumn get archived => $composableBuilder(column: $table.archived, builder: (column) => column); + GeneratedColumn get groupName => + $composableBuilder(column: $table.groupName, builder: (column) => column); + GeneratedColumn get lastMessageExchange => $composableBuilder( column: $table.lastMessageExchange, builder: (column) => column); @@ -7063,6 +7155,7 @@ class $$GroupsTableTableManager extends RootTableManager< Value isGroupOfTwo = const Value.absent(), Value pinned = const Value.absent(), Value archived = const Value.absent(), + Value groupName = const Value.absent(), Value lastMessageExchange = const Value.absent(), Value createdAt = const Value.absent(), Value 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 pinned = const Value.absent(), Value archived = const Value.absent(), + required String groupName, Value lastMessageExchange = const Value.absent(), Value createdAt = const Value.absent(), Value 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 senderId, Value content, Value mediaId, + Value mediaStored, Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, @@ -7574,6 +7671,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value mediaStored, Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, @@ -7716,6 +7814,9 @@ class $$MessagesTableFilterComposer ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); + ColumnFilters get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnFilters(column)); @@ -7904,6 +8005,9 @@ class $$MessagesTableOrderingComposer ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => ColumnOrderings(column)); @@ -8030,6 +8134,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); + GeneratedColumn get mediaStored => $composableBuilder( + column: $table.mediaStored, builder: (column) => column); + GeneratedColumn get downloadToken => $composableBuilder( column: $table.downloadToken, builder: (column) => column); @@ -8236,6 +8343,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value mediaStored = const Value.absent(), Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value 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 senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value mediaStored = const Value.absent(), Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value 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, diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 64bf2b5..a30e8cc 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -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(); diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart index dce9a1c..018d17b 100644 --- a/lib/src/services/api/mediafiles/media_background.service.dart +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -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 initFileDownloader() async { ); } } + +Future 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); +} diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index d0f4350..de6c9f3 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -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 checkForFailedUploads() async { -// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); -// final mediaUploadIds = []; -// 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 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(() 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 initializeMediaUpload( MediaType type, @@ -141,78 +38,10 @@ Future initializeMediaUpload( return MediaFileService.fromMedia(mediaFile); } -Future 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 encryptMediaFiles( - int mediaUploadId, - Future imageHandler, - Future? 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 finalizeUpload( +Future insertMediaFileInMessagesTable( MediaFileService mediaService, List groupIds, ) async { - final messageIds = []; - for (final groupId in groupIds) { final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( @@ -221,7 +50,6 @@ Future 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 finalizeUpload( } } - unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId)); + unawaited(startBackgroundMediaUpload(mediaService)); } -final lockingHandleNextMediaUploadStep = Mutex(); -Future 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 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 _encryptMediaFiles(MediaFileService mediaService) async { + /// if there is a video wait until it is finished with compression -Future 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 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 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 handleMediaUpload(MediaUpload media) async { - final bytesToUpload = - await readSendMediaFile(media.mediaUploadId, 'encrypted'); - - if (media.messageIds == null) return; - - final messageIds = media.messageIds!; - +Future _createUploadRequest(MediaFileService media) async { final downloadTokens = []; final messagesOnSuccess = []; - 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 handleMediaUpload(MediaUpload media) async { final uploadRequestBytes = uploadRequest.writeToBuffer(); + await media.uploadRequestPath.writeAsBytes(uploadRequestBytes); +} + +Future _uploadUploadRequest(MediaFileService media) async { final apiAuthTokenRaw = await const FlutterSecureStorage() .read(key: SecureStorageKeys.apiAuthToken); if (apiAuthTokenRaw == null) { @@ -477,108 +207,27 @@ Future 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 currentUploadTasks = {}; - -Future 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 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 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); } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index a76fcc3..c7a84dd 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -30,18 +30,20 @@ Future tryTransmitMessages() async { }); } -Future 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 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 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 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 tryToSendCompleteMessage({ receipt.contactId, const ContactsCompanion(deleted: Value(true)), ); - return; + return null; } } @@ -149,12 +155,14 @@ Future tryToSendCompleteMessage({ await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); } } + return null; } -Future 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 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 notifyContactAboutOpeningMessage( diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 13ce178..2a062d2 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -49,6 +49,26 @@ class MediaFileService { await updateFromDB(); } + Future setUploadState(UploadState uploadState) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + uploadState: Value(uploadState), + ), + ); + await updateFromDB(); + } + + Future setEncryptedMac(Uint8List encryptionMac) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + encryptionMac: Value(encryptionMac), + ), + ); + await updateFromDB(); + } + Future 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', diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index a675212..360a375 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -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 customLocalPushNotification(String title, String msg) async { ); await flutterLocalNotificationsPlugin.show( - gMediaShowInfinite + Random.secure().nextInt(9999), + Random.secure().nextInt(9999), title, msg, notificationDetails, diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 5a6426e..630ef5d 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -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 enableTwonlySafe(String password) async { diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index 097af4a..138c623 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -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'; diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index b10f65f..6824617 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.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 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 { }); String? res; - var memoryPath = await getMediaBaseFilePath('memories'); - if (widget.mediaUploadId != null) { - memoryPath = join(memoryPath, '${widget.mediaUploadId!}'); - } else { - final random = Random(); - final token = uint8ListToHex( - List.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; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 20eb318..b4bb4e4 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -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 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 Function( int sCameraId, bool init, @@ -328,7 +326,7 @@ class _CameraPreviewViewState extends State { 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 { 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 { }); 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 { ), ), 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 { videoRecordingStarted: videoRecordingStarted, maxVideoRecordingTime: maxVideoRecordingTime, ), - if (!sharePreviewIsShown && widget.sendTo != null) + if (!sharePreviewIsShown && widget.sendToGroup != null) Positioned( left: 5, top: 10, diff --git a/lib/src/views/camera/camera_send_to_view.dart b/lib/src/views/camera/camera_send_to_view.dart index e00d990..8586b4d 100644 --- a/lib/src/views/camera/camera_send_to_view.dart +++ b/lib/src/views/camera/camera_send_to_view.dart @@ -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 createState() => CameraSendToViewState(); } @@ -77,7 +77,7 @@ class CameraSendToViewState extends State { ), CameraPreviewControllerView( selectCamera: selectCamera, - sendTo: widget.sendTo, + sendToGroup: widget.sendToGroup, cameraController: cameraController, selectedCameraDetails: selectedCameraDetails, screenshotController: screenshotController, diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 94fbd49..ff66dc6 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -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 layers = []; List undoLayers = []; List 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? imageBytesFuture; - final Group? sendTo; + final Group? sendToGroup; final bool sharedFromGallery; final MediaFileService mediaFileService; @override @@ -66,9 +58,6 @@ class _ShareImageEditorView extends State { ImageItem currentImage = ImageItem(); ScreenshotController screenshotController = ScreenshotController(); - /// Media upload variables - Future? videoUploadHandler; - MediaFileService get mediaService => widget.mediaFileService; MediaFile get media => widget.mediaFileService.mediaFile; @@ -78,8 +67,8 @@ class _ShareImageEditorView extends State { 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 { } Future 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 { } } - Future storeImageAsOriginal() async { + Future 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 { ); 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 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 loadImage(Future imageBytesFuture) async { @@ -377,14 +374,10 @@ class _ShareImageEditorView extends State { 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 { 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 { 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 { : 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 { ), ), label: Text( - (widget.sendTo == null) + (widget.sendToGroup == null) ? context.lang.shareImagedEditorShareWith - : getContactDisplayName(widget.sendTo!), + : widget.sendToGroup!.groupName, style: const TextStyle(fontSize: 17), ), ), diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 22fc367..13629ef 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -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 imageBytesFuture; - final HashSet selectedUserIds; - final bool? enableVideoAudio; - final void Function(int, bool) updateStatus; - final Future? videoUploadHandler; + final HashSet selectedGroupIds; + final void Function(String, bool) updateSelectedGroupIds; + final Future? mediaStoreFuture; final MediaFileService mediaFileService; @override @@ -69,17 +66,11 @@ class _ShareImageView extends State { } Future 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(() {}); }