media upload

This commit is contained in:
otsmr 2025-10-23 21:31:13 +02:00
parent b2dc384465
commit 645dfe16da
20 changed files with 456 additions and 647 deletions

View file

@ -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/image_editor.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.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/media_background.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.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/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';

View file

@ -26,4 +26,9 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
await (update(groups)..where((c) => c.groupId.equals(groupId))) await (update(groups)..where((c) => c.groupId.equals(groupId)))
.write(updates); .write(updates);
} }
Future<List<GroupMember>> getGroupMembers(String groupId) async {
return (select(groupMembers)..where((t) => t.groupId.equals(groupId)))
.get();
}
} }

View file

@ -285,6 +285,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.write(updatedValues); .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 { Future<Message?> insertMessage(MessagesCompanion message) async {
try { try {
final rowId = await into(messages).insert(message); final rowId = await into(messages).insert(message);

View file

@ -11,6 +11,8 @@ class Groups extends Table {
BoolColumn get pinned => boolean().withDefault(const Constant(false))(); BoolColumn get pinned => boolean().withDefault(const Constant(false))();
BoolColumn get archived => boolean().withDefault(const Constant(false))(); BoolColumn get archived => boolean().withDefault(const Constant(false))();
TextColumn get groupName => text()();
DateTimeColumn get lastMessageExchange => DateTimeColumn get lastMessageExchange =>
dateTime().withDefault(currentDateAndTime)(); dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();

View file

@ -15,8 +15,7 @@ enum UploadState {
// Image was stored but not send // Image was stored but not send
storedOnly, storedOnly,
// At this point the user is finished with editing, and the media file can be uploaded // At this point the user is finished with editing, and the media file can be uploaded
compressing, preprocessing,
encrypting,
uploading, uploading,
backgroundUploadTaskStarted, backgroundUploadTaskStarted,
uploaded, uploaded,

View file

@ -18,6 +18,8 @@ class Messages extends Table {
TextColumn get mediaId => TextColumn get mediaId =>
text().nullable().references(MediaFiles, #mediaId)(); text().nullable().references(MediaFiles, #mediaId)();
BoolColumn get mediaStored => boolean().withDefault(const Constant(false))();
BlobColumn get downloadToken => blob().nullable()(); BlobColumn get downloadToken => blob().nullable()();
TextColumn get quotesMessageId => TextColumn get quotesMessageId =>

View file

@ -1061,6 +1061,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'), GeneratedColumn.constraintIsAlways('CHECK ("archived" IN (0, 1))'),
defaultValue: const Constant(false)); 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 = static const VerificationMeta _lastMessageExchangeMeta =
const VerificationMeta('lastMessageExchange'); const VerificationMeta('lastMessageExchange');
@override @override
@ -1084,6 +1090,7 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
isGroupOfTwo, isGroupOfTwo,
pinned, pinned,
archived, archived,
groupName,
lastMessageExchange, lastMessageExchange,
createdAt createdAt
]; ];
@ -1125,6 +1132,12 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
context.handle(_archivedMeta, context.handle(_archivedMeta,
archived.isAcceptableOrUnknown(data['archived']!, _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')) { if (data.containsKey('last_message_exchange')) {
context.handle( context.handle(
_lastMessageExchangeMeta, _lastMessageExchangeMeta,
@ -1154,6 +1167,8 @@ class $GroupsTable extends Groups with TableInfo<$GroupsTable, Group> {
.read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!, .read(DriftSqlType.bool, data['${effectivePrefix}pinned'])!,
archived: attachedDatabase.typeMapping archived: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}archived'])!, .read(DriftSqlType.bool, data['${effectivePrefix}archived'])!,
groupName: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}group_name'])!,
lastMessageExchange: attachedDatabase.typeMapping.read( lastMessageExchange: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}last_message_exchange'])!, data['${effectivePrefix}last_message_exchange'])!,
@ -1174,6 +1189,7 @@ class Group extends DataClass implements Insertable<Group> {
final bool isGroupOfTwo; final bool isGroupOfTwo;
final bool pinned; final bool pinned;
final bool archived; final bool archived;
final String groupName;
final DateTime lastMessageExchange; final DateTime lastMessageExchange;
final DateTime createdAt; final DateTime createdAt;
const Group( const Group(
@ -1182,6 +1198,7 @@ class Group extends DataClass implements Insertable<Group> {
required this.isGroupOfTwo, required this.isGroupOfTwo,
required this.pinned, required this.pinned,
required this.archived, required this.archived,
required this.groupName,
required this.lastMessageExchange, required this.lastMessageExchange,
required this.createdAt}); required this.createdAt});
@override @override
@ -1192,6 +1209,7 @@ class Group extends DataClass implements Insertable<Group> {
map['is_group_of_two'] = Variable<bool>(isGroupOfTwo); map['is_group_of_two'] = Variable<bool>(isGroupOfTwo);
map['pinned'] = Variable<bool>(pinned); map['pinned'] = Variable<bool>(pinned);
map['archived'] = Variable<bool>(archived); map['archived'] = Variable<bool>(archived);
map['group_name'] = Variable<String>(groupName);
map['last_message_exchange'] = Variable<DateTime>(lastMessageExchange); map['last_message_exchange'] = Variable<DateTime>(lastMessageExchange);
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
return map; return map;
@ -1204,6 +1222,7 @@ class Group extends DataClass implements Insertable<Group> {
isGroupOfTwo: Value(isGroupOfTwo), isGroupOfTwo: Value(isGroupOfTwo),
pinned: Value(pinned), pinned: Value(pinned),
archived: Value(archived), archived: Value(archived),
groupName: Value(groupName),
lastMessageExchange: Value(lastMessageExchange), lastMessageExchange: Value(lastMessageExchange),
createdAt: Value(createdAt), createdAt: Value(createdAt),
); );
@ -1218,6 +1237,7 @@ class Group extends DataClass implements Insertable<Group> {
isGroupOfTwo: serializer.fromJson<bool>(json['isGroupOfTwo']), isGroupOfTwo: serializer.fromJson<bool>(json['isGroupOfTwo']),
pinned: serializer.fromJson<bool>(json['pinned']), pinned: serializer.fromJson<bool>(json['pinned']),
archived: serializer.fromJson<bool>(json['archived']), archived: serializer.fromJson<bool>(json['archived']),
groupName: serializer.fromJson<String>(json['groupName']),
lastMessageExchange: lastMessageExchange:
serializer.fromJson<DateTime>(json['lastMessageExchange']), serializer.fromJson<DateTime>(json['lastMessageExchange']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
@ -1232,6 +1252,7 @@ class Group extends DataClass implements Insertable<Group> {
'isGroupOfTwo': serializer.toJson<bool>(isGroupOfTwo), 'isGroupOfTwo': serializer.toJson<bool>(isGroupOfTwo),
'pinned': serializer.toJson<bool>(pinned), 'pinned': serializer.toJson<bool>(pinned),
'archived': serializer.toJson<bool>(archived), 'archived': serializer.toJson<bool>(archived),
'groupName': serializer.toJson<String>(groupName),
'lastMessageExchange': serializer.toJson<DateTime>(lastMessageExchange), 'lastMessageExchange': serializer.toJson<DateTime>(lastMessageExchange),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
}; };
@ -1243,6 +1264,7 @@ class Group extends DataClass implements Insertable<Group> {
bool? isGroupOfTwo, bool? isGroupOfTwo,
bool? pinned, bool? pinned,
bool? archived, bool? archived,
String? groupName,
DateTime? lastMessageExchange, DateTime? lastMessageExchange,
DateTime? createdAt}) => DateTime? createdAt}) =>
Group( Group(
@ -1251,6 +1273,7 @@ class Group extends DataClass implements Insertable<Group> {
isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo,
pinned: pinned ?? this.pinned, pinned: pinned ?? this.pinned,
archived: archived ?? this.archived, archived: archived ?? this.archived,
groupName: groupName ?? this.groupName,
lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
); );
@ -1265,6 +1288,7 @@ class Group extends DataClass implements Insertable<Group> {
: this.isGroupOfTwo, : this.isGroupOfTwo,
pinned: data.pinned.present ? data.pinned.value : this.pinned, pinned: data.pinned.present ? data.pinned.value : this.pinned,
archived: data.archived.present ? data.archived.value : this.archived, archived: data.archived.present ? data.archived.value : this.archived,
groupName: data.groupName.present ? data.groupName.value : this.groupName,
lastMessageExchange: data.lastMessageExchange.present lastMessageExchange: data.lastMessageExchange.present
? data.lastMessageExchange.value ? data.lastMessageExchange.value
: this.lastMessageExchange, : this.lastMessageExchange,
@ -1280,6 +1304,7 @@ class Group extends DataClass implements Insertable<Group> {
..write('isGroupOfTwo: $isGroupOfTwo, ') ..write('isGroupOfTwo: $isGroupOfTwo, ')
..write('pinned: $pinned, ') ..write('pinned: $pinned, ')
..write('archived: $archived, ') ..write('archived: $archived, ')
..write('groupName: $groupName, ')
..write('lastMessageExchange: $lastMessageExchange, ') ..write('lastMessageExchange: $lastMessageExchange, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt')
..write(')')) ..write(')'))
@ -1288,7 +1313,7 @@ class Group extends DataClass implements Insertable<Group> {
@override @override
int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned, int get hashCode => Object.hash(groupId, isGroupAdmin, isGroupOfTwo, pinned,
archived, lastMessageExchange, createdAt); archived, groupName, lastMessageExchange, createdAt);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -1298,6 +1323,7 @@ class Group extends DataClass implements Insertable<Group> {
other.isGroupOfTwo == this.isGroupOfTwo && other.isGroupOfTwo == this.isGroupOfTwo &&
other.pinned == this.pinned && other.pinned == this.pinned &&
other.archived == this.archived && other.archived == this.archived &&
other.groupName == this.groupName &&
other.lastMessageExchange == this.lastMessageExchange && other.lastMessageExchange == this.lastMessageExchange &&
other.createdAt == this.createdAt); other.createdAt == this.createdAt);
} }
@ -1308,6 +1334,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
final Value<bool> isGroupOfTwo; final Value<bool> isGroupOfTwo;
final Value<bool> pinned; final Value<bool> pinned;
final Value<bool> archived; final Value<bool> archived;
final Value<String> groupName;
final Value<DateTime> lastMessageExchange; final Value<DateTime> lastMessageExchange;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<int> rowid; final Value<int> rowid;
@ -1317,6 +1344,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
this.isGroupOfTwo = const Value.absent(), this.isGroupOfTwo = const Value.absent(),
this.pinned = const Value.absent(), this.pinned = const Value.absent(),
this.archived = const Value.absent(), this.archived = const Value.absent(),
this.groupName = const Value.absent(),
this.lastMessageExchange = const Value.absent(), this.lastMessageExchange = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
@ -1327,17 +1355,20 @@ class GroupsCompanion extends UpdateCompanion<Group> {
required bool isGroupOfTwo, required bool isGroupOfTwo,
this.pinned = const Value.absent(), this.pinned = const Value.absent(),
this.archived = const Value.absent(), this.archived = const Value.absent(),
required String groupName,
this.lastMessageExchange = const Value.absent(), this.lastMessageExchange = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : isGroupAdmin = Value(isGroupAdmin), }) : isGroupAdmin = Value(isGroupAdmin),
isGroupOfTwo = Value(isGroupOfTwo); isGroupOfTwo = Value(isGroupOfTwo),
groupName = Value(groupName);
static Insertable<Group> custom({ static Insertable<Group> custom({
Expression<String>? groupId, Expression<String>? groupId,
Expression<bool>? isGroupAdmin, Expression<bool>? isGroupAdmin,
Expression<bool>? isGroupOfTwo, Expression<bool>? isGroupOfTwo,
Expression<bool>? pinned, Expression<bool>? pinned,
Expression<bool>? archived, Expression<bool>? archived,
Expression<String>? groupName,
Expression<DateTime>? lastMessageExchange, Expression<DateTime>? lastMessageExchange,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<int>? rowid, Expression<int>? rowid,
@ -1348,6 +1379,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo, if (isGroupOfTwo != null) 'is_group_of_two': isGroupOfTwo,
if (pinned != null) 'pinned': pinned, if (pinned != null) 'pinned': pinned,
if (archived != null) 'archived': archived, if (archived != null) 'archived': archived,
if (groupName != null) 'group_name': groupName,
if (lastMessageExchange != null) if (lastMessageExchange != null)
'last_message_exchange': lastMessageExchange, 'last_message_exchange': lastMessageExchange,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
@ -1361,6 +1393,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
Value<bool>? isGroupOfTwo, Value<bool>? isGroupOfTwo,
Value<bool>? pinned, Value<bool>? pinned,
Value<bool>? archived, Value<bool>? archived,
Value<String>? groupName,
Value<DateTime>? lastMessageExchange, Value<DateTime>? lastMessageExchange,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<int>? rowid}) { Value<int>? rowid}) {
@ -1370,6 +1403,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo, isGroupOfTwo: isGroupOfTwo ?? this.isGroupOfTwo,
pinned: pinned ?? this.pinned, pinned: pinned ?? this.pinned,
archived: archived ?? this.archived, archived: archived ?? this.archived,
groupName: groupName ?? this.groupName,
lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange, lastMessageExchange: lastMessageExchange ?? this.lastMessageExchange,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
@ -1394,6 +1428,9 @@ class GroupsCompanion extends UpdateCompanion<Group> {
if (archived.present) { if (archived.present) {
map['archived'] = Variable<bool>(archived.value); map['archived'] = Variable<bool>(archived.value);
} }
if (groupName.present) {
map['group_name'] = Variable<String>(groupName.value);
}
if (lastMessageExchange.present) { if (lastMessageExchange.present) {
map['last_message_exchange'] = map['last_message_exchange'] =
Variable<DateTime>(lastMessageExchange.value); Variable<DateTime>(lastMessageExchange.value);
@ -1415,6 +1452,7 @@ class GroupsCompanion extends UpdateCompanion<Group> {
..write('isGroupOfTwo: $isGroupOfTwo, ') ..write('isGroupOfTwo: $isGroupOfTwo, ')
..write('pinned: $pinned, ') ..write('pinned: $pinned, ')
..write('archived: $archived, ') ..write('archived: $archived, ')
..write('groupName: $groupName, ')
..write('lastMessageExchange: $lastMessageExchange, ') ..write('lastMessageExchange: $lastMessageExchange, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
@ -2231,6 +2269,16 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES media_files (media_id)')); '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 = static const VerificationMeta _downloadTokenMeta =
const VerificationMeta('downloadToken'); const VerificationMeta('downloadToken');
@override @override
@ -2323,6 +2371,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
senderId, senderId,
content, content,
mediaId, mediaId,
mediaStored,
downloadToken, downloadToken,
quotesMessageId, quotesMessageId,
isDeletedFromSender, isDeletedFromSender,
@ -2366,6 +2415,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
context.handle(_mediaIdMeta, context.handle(_mediaIdMeta,
mediaId.isAcceptableOrUnknown(data['media_id']!, _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')) { if (data.containsKey('download_token')) {
context.handle( context.handle(
_downloadTokenMeta, _downloadTokenMeta,
@ -2439,6 +2494,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
.read(DriftSqlType.string, data['${effectivePrefix}content']), .read(DriftSqlType.string, data['${effectivePrefix}content']),
mediaId: attachedDatabase.typeMapping mediaId: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}media_id']), .read(DriftSqlType.string, data['${effectivePrefix}media_id']),
mediaStored: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}media_stored'])!,
downloadToken: attachedDatabase.typeMapping downloadToken: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}download_token']), .read(DriftSqlType.blob, data['${effectivePrefix}download_token']),
quotesMessageId: attachedDatabase.typeMapping.read( quotesMessageId: attachedDatabase.typeMapping.read(
@ -2474,6 +2531,7 @@ class Message extends DataClass implements Insertable<Message> {
final int? senderId; final int? senderId;
final String? content; final String? content;
final String? mediaId; final String? mediaId;
final bool mediaStored;
final Uint8List? downloadToken; final Uint8List? downloadToken;
final String? quotesMessageId; final String? quotesMessageId;
final bool isDeletedFromSender; final bool isDeletedFromSender;
@ -2490,6 +2548,7 @@ class Message extends DataClass implements Insertable<Message> {
this.senderId, this.senderId,
this.content, this.content,
this.mediaId, this.mediaId,
required this.mediaStored,
this.downloadToken, this.downloadToken,
this.quotesMessageId, this.quotesMessageId,
required this.isDeletedFromSender, required this.isDeletedFromSender,
@ -2514,6 +2573,7 @@ class Message extends DataClass implements Insertable<Message> {
if (!nullToAbsent || mediaId != null) { if (!nullToAbsent || mediaId != null) {
map['media_id'] = Variable<String>(mediaId); map['media_id'] = Variable<String>(mediaId);
} }
map['media_stored'] = Variable<bool>(mediaStored);
if (!nullToAbsent || downloadToken != null) { if (!nullToAbsent || downloadToken != null) {
map['download_token'] = Variable<Uint8List>(downloadToken); map['download_token'] = Variable<Uint8List>(downloadToken);
} }
@ -2548,6 +2608,7 @@ class Message extends DataClass implements Insertable<Message> {
mediaId: mediaId == null && nullToAbsent mediaId: mediaId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(mediaId), : Value(mediaId),
mediaStored: Value(mediaStored),
downloadToken: downloadToken == null && nullToAbsent downloadToken: downloadToken == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(downloadToken), : Value(downloadToken),
@ -2578,6 +2639,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId: serializer.fromJson<int?>(json['senderId']), senderId: serializer.fromJson<int?>(json['senderId']),
content: serializer.fromJson<String?>(json['content']), content: serializer.fromJson<String?>(json['content']),
mediaId: serializer.fromJson<String?>(json['mediaId']), mediaId: serializer.fromJson<String?>(json['mediaId']),
mediaStored: serializer.fromJson<bool>(json['mediaStored']),
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']), downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']), quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']),
isDeletedFromSender: isDeletedFromSender:
@ -2600,6 +2662,7 @@ class Message extends DataClass implements Insertable<Message> {
'senderId': serializer.toJson<int?>(senderId), 'senderId': serializer.toJson<int?>(senderId),
'content': serializer.toJson<String?>(content), 'content': serializer.toJson<String?>(content),
'mediaId': serializer.toJson<String?>(mediaId), 'mediaId': serializer.toJson<String?>(mediaId),
'mediaStored': serializer.toJson<bool>(mediaStored),
'downloadToken': serializer.toJson<Uint8List?>(downloadToken), 'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
'quotesMessageId': serializer.toJson<String?>(quotesMessageId), 'quotesMessageId': serializer.toJson<String?>(quotesMessageId),
'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender), 'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender),
@ -2619,6 +2682,7 @@ class Message extends DataClass implements Insertable<Message> {
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
bool? mediaStored,
Value<Uint8List?> downloadToken = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(), Value<String?> quotesMessageId = const Value.absent(),
bool? isDeletedFromSender, bool? isDeletedFromSender,
@ -2635,6 +2699,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId: senderId.present ? senderId.value : this.senderId, senderId: senderId.present ? senderId.value : this.senderId,
content: content.present ? content.value : this.content, content: content.present ? content.value : this.content,
mediaId: mediaId.present ? mediaId.value : this.mediaId, mediaId: mediaId.present ? mediaId.value : this.mediaId,
mediaStored: mediaStored ?? this.mediaStored,
downloadToken: downloadToken:
downloadToken.present ? downloadToken.value : this.downloadToken, downloadToken.present ? downloadToken.value : this.downloadToken,
quotesMessageId: quotesMessageId.present quotesMessageId: quotesMessageId.present
@ -2656,6 +2721,8 @@ class Message extends DataClass implements Insertable<Message> {
senderId: data.senderId.present ? data.senderId.value : this.senderId, senderId: data.senderId.present ? data.senderId.value : this.senderId,
content: data.content.present ? data.content.value : this.content, content: data.content.present ? data.content.value : this.content,
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
mediaStored:
data.mediaStored.present ? data.mediaStored.value : this.mediaStored,
downloadToken: data.downloadToken.present downloadToken: data.downloadToken.present
? data.downloadToken.value ? data.downloadToken.value
: this.downloadToken, : this.downloadToken,
@ -2687,6 +2754,7 @@ class Message extends DataClass implements Insertable<Message> {
..write('senderId: $senderId, ') ..write('senderId: $senderId, ')
..write('content: $content, ') ..write('content: $content, ')
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('mediaStored: $mediaStored, ')
..write('downloadToken: $downloadToken, ') ..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ') ..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isDeletedFromSender: $isDeletedFromSender, ')
@ -2708,6 +2776,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId, senderId,
content, content,
mediaId, mediaId,
mediaStored,
$driftBlobEquality.hash(downloadToken), $driftBlobEquality.hash(downloadToken),
quotesMessageId, quotesMessageId,
isDeletedFromSender, isDeletedFromSender,
@ -2727,6 +2796,7 @@ class Message extends DataClass implements Insertable<Message> {
other.senderId == this.senderId && other.senderId == this.senderId &&
other.content == this.content && other.content == this.content &&
other.mediaId == this.mediaId && other.mediaId == this.mediaId &&
other.mediaStored == this.mediaStored &&
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) && $driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
other.quotesMessageId == this.quotesMessageId && other.quotesMessageId == this.quotesMessageId &&
other.isDeletedFromSender == this.isDeletedFromSender && other.isDeletedFromSender == this.isDeletedFromSender &&
@ -2745,6 +2815,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<int?> senderId; final Value<int?> senderId;
final Value<String?> content; final Value<String?> content;
final Value<String?> mediaId; final Value<String?> mediaId;
final Value<bool> mediaStored;
final Value<Uint8List?> downloadToken; final Value<Uint8List?> downloadToken;
final Value<String?> quotesMessageId; final Value<String?> quotesMessageId;
final Value<bool> isDeletedFromSender; final Value<bool> isDeletedFromSender;
@ -2762,6 +2833,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.mediaStored = const Value.absent(),
this.downloadToken = const Value.absent(), this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(), this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(), this.isDeletedFromSender = const Value.absent(),
@ -2780,6 +2852,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.mediaStored = const Value.absent(),
this.downloadToken = const Value.absent(), this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(), this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(), this.isDeletedFromSender = const Value.absent(),
@ -2798,6 +2871,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Expression<int>? senderId, Expression<int>? senderId,
Expression<String>? content, Expression<String>? content,
Expression<String>? mediaId, Expression<String>? mediaId,
Expression<bool>? mediaStored,
Expression<Uint8List>? downloadToken, Expression<Uint8List>? downloadToken,
Expression<String>? quotesMessageId, Expression<String>? quotesMessageId,
Expression<bool>? isDeletedFromSender, Expression<bool>? isDeletedFromSender,
@ -2816,6 +2890,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (senderId != null) 'sender_id': senderId, if (senderId != null) 'sender_id': senderId,
if (content != null) 'content': content, if (content != null) 'content': content,
if (mediaId != null) 'media_id': mediaId, if (mediaId != null) 'media_id': mediaId,
if (mediaStored != null) 'media_stored': mediaStored,
if (downloadToken != null) 'download_token': downloadToken, if (downloadToken != null) 'download_token': downloadToken,
if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, if (quotesMessageId != null) 'quotes_message_id': quotesMessageId,
if (isDeletedFromSender != null) if (isDeletedFromSender != null)
@ -2837,6 +2912,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Value<int?>? senderId, Value<int?>? senderId,
Value<String?>? content, Value<String?>? content,
Value<String?>? mediaId, Value<String?>? mediaId,
Value<bool>? mediaStored,
Value<Uint8List?>? downloadToken, Value<Uint8List?>? downloadToken,
Value<String?>? quotesMessageId, Value<String?>? quotesMessageId,
Value<bool>? isDeletedFromSender, Value<bool>? isDeletedFromSender,
@ -2854,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
senderId: senderId ?? this.senderId, senderId: senderId ?? this.senderId,
content: content ?? this.content, content: content ?? this.content,
mediaId: mediaId ?? this.mediaId, mediaId: mediaId ?? this.mediaId,
mediaStored: mediaStored ?? this.mediaStored,
downloadToken: downloadToken ?? this.downloadToken, downloadToken: downloadToken ?? this.downloadToken,
quotesMessageId: quotesMessageId ?? this.quotesMessageId, quotesMessageId: quotesMessageId ?? this.quotesMessageId,
isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender,
@ -2886,6 +2963,9 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (mediaId.present) { if (mediaId.present) {
map['media_id'] = Variable<String>(mediaId.value); map['media_id'] = Variable<String>(mediaId.value);
} }
if (mediaStored.present) {
map['media_stored'] = Variable<bool>(mediaStored.value);
}
if (downloadToken.present) { if (downloadToken.present) {
map['download_token'] = Variable<Uint8List>(downloadToken.value); map['download_token'] = Variable<Uint8List>(downloadToken.value);
} }
@ -2930,6 +3010,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
..write('senderId: $senderId, ') ..write('senderId: $senderId, ')
..write('content: $content, ') ..write('content: $content, ')
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('mediaStored: $mediaStored, ')
..write('downloadToken: $downloadToken, ') ..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ') ..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isDeletedFromSender: $isDeletedFromSender, ')
@ -6863,6 +6944,7 @@ typedef $$GroupsTableCreateCompanionBuilder = GroupsCompanion Function({
required bool isGroupOfTwo, required bool isGroupOfTwo,
Value<bool> pinned, Value<bool> pinned,
Value<bool> archived, Value<bool> archived,
required String groupName,
Value<DateTime> lastMessageExchange, Value<DateTime> lastMessageExchange,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<int> rowid, Value<int> rowid,
@ -6873,6 +6955,7 @@ typedef $$GroupsTableUpdateCompanionBuilder = GroupsCompanion Function({
Value<bool> isGroupOfTwo, Value<bool> isGroupOfTwo,
Value<bool> pinned, Value<bool> pinned,
Value<bool> archived, Value<bool> archived,
Value<String> groupName,
Value<DateTime> lastMessageExchange, Value<DateTime> lastMessageExchange,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<int> rowid, Value<int> rowid,
@ -6921,6 +7004,9 @@ class $$GroupsTableFilterComposer extends Composer<_$TwonlyDB, $GroupsTable> {
ColumnFilters<bool> get archived => $composableBuilder( ColumnFilters<bool> get archived => $composableBuilder(
column: $table.archived, builder: (column) => ColumnFilters(column)); column: $table.archived, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get groupName => $composableBuilder(
column: $table.groupName, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get lastMessageExchange => $composableBuilder( ColumnFilters<DateTime> get lastMessageExchange => $composableBuilder(
column: $table.lastMessageExchange, column: $table.lastMessageExchange,
builder: (column) => ColumnFilters(column)); builder: (column) => ColumnFilters(column));
@ -6975,6 +7061,9 @@ class $$GroupsTableOrderingComposer extends Composer<_$TwonlyDB, $GroupsTable> {
ColumnOrderings<bool> get archived => $composableBuilder( ColumnOrderings<bool> get archived => $composableBuilder(
column: $table.archived, builder: (column) => ColumnOrderings(column)); column: $table.archived, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get groupName => $composableBuilder(
column: $table.groupName, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get lastMessageExchange => $composableBuilder( ColumnOrderings<DateTime> get lastMessageExchange => $composableBuilder(
column: $table.lastMessageExchange, column: $table.lastMessageExchange,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -7007,6 +7096,9 @@ class $$GroupsTableAnnotationComposer
GeneratedColumn<bool> get archived => GeneratedColumn<bool> get archived =>
$composableBuilder(column: $table.archived, builder: (column) => column); $composableBuilder(column: $table.archived, builder: (column) => column);
GeneratedColumn<String> get groupName =>
$composableBuilder(column: $table.groupName, builder: (column) => column);
GeneratedColumn<DateTime> get lastMessageExchange => $composableBuilder( GeneratedColumn<DateTime> get lastMessageExchange => $composableBuilder(
column: $table.lastMessageExchange, builder: (column) => column); column: $table.lastMessageExchange, builder: (column) => column);
@ -7063,6 +7155,7 @@ class $$GroupsTableTableManager extends RootTableManager<
Value<bool> isGroupOfTwo = const Value.absent(), Value<bool> isGroupOfTwo = const Value.absent(),
Value<bool> pinned = const Value.absent(), Value<bool> pinned = const Value.absent(),
Value<bool> archived = const Value.absent(), Value<bool> archived = const Value.absent(),
Value<String> groupName = const Value.absent(),
Value<DateTime> lastMessageExchange = const Value.absent(), Value<DateTime> lastMessageExchange = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -7073,6 +7166,7 @@ class $$GroupsTableTableManager extends RootTableManager<
isGroupOfTwo: isGroupOfTwo, isGroupOfTwo: isGroupOfTwo,
pinned: pinned, pinned: pinned,
archived: archived, archived: archived,
groupName: groupName,
lastMessageExchange: lastMessageExchange, lastMessageExchange: lastMessageExchange,
createdAt: createdAt, createdAt: createdAt,
rowid: rowid, rowid: rowid,
@ -7083,6 +7177,7 @@ class $$GroupsTableTableManager extends RootTableManager<
required bool isGroupOfTwo, required bool isGroupOfTwo,
Value<bool> pinned = const Value.absent(), Value<bool> pinned = const Value.absent(),
Value<bool> archived = const Value.absent(), Value<bool> archived = const Value.absent(),
required String groupName,
Value<DateTime> lastMessageExchange = const Value.absent(), Value<DateTime> lastMessageExchange = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
@ -7093,6 +7188,7 @@ class $$GroupsTableTableManager extends RootTableManager<
isGroupOfTwo: isGroupOfTwo, isGroupOfTwo: isGroupOfTwo,
pinned: pinned, pinned: pinned,
archived: archived, archived: archived,
groupName: groupName,
lastMessageExchange: lastMessageExchange, lastMessageExchange: lastMessageExchange,
createdAt: createdAt, createdAt: createdAt,
rowid: rowid, rowid: rowid,
@ -7556,6 +7652,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
Value<int?> senderId, Value<int?> senderId,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<bool> mediaStored,
Value<Uint8List?> downloadToken, Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId, Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender, Value<bool> isDeletedFromSender,
@ -7574,6 +7671,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<int?> senderId, Value<int?> senderId,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<bool> mediaStored,
Value<Uint8List?> downloadToken, Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId, Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender, Value<bool> isDeletedFromSender,
@ -7716,6 +7814,9 @@ class $$MessagesTableFilterComposer
ColumnFilters<String> get content => $composableBuilder( ColumnFilters<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnFilters(column)); column: $table.content, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get mediaStored => $composableBuilder(
column: $table.mediaStored, builder: (column) => ColumnFilters(column));
ColumnFilters<Uint8List> get downloadToken => $composableBuilder( ColumnFilters<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => ColumnFilters(column)); column: $table.downloadToken, builder: (column) => ColumnFilters(column));
@ -7904,6 +8005,9 @@ class $$MessagesTableOrderingComposer
ColumnOrderings<String> get content => $composableBuilder( ColumnOrderings<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnOrderings(column)); column: $table.content, builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get mediaStored => $composableBuilder(
column: $table.mediaStored, builder: (column) => ColumnOrderings(column));
ColumnOrderings<Uint8List> get downloadToken => $composableBuilder( ColumnOrderings<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, column: $table.downloadToken,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -8030,6 +8134,9 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<String> get content => GeneratedColumn<String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column); $composableBuilder(column: $table.content, builder: (column) => column);
GeneratedColumn<bool> get mediaStored => $composableBuilder(
column: $table.mediaStored, builder: (column) => column);
GeneratedColumn<Uint8List> get downloadToken => $composableBuilder( GeneratedColumn<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => column); column: $table.downloadToken, builder: (column) => column);
@ -8236,6 +8343,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<bool> mediaStored = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(), Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(), Value<bool> isDeletedFromSender = const Value.absent(),
@ -8254,6 +8362,7 @@ class $$MessagesTableTableManager extends RootTableManager<
senderId: senderId, senderId: senderId,
content: content, content: content,
mediaId: mediaId, mediaId: mediaId,
mediaStored: mediaStored,
downloadToken: downloadToken, downloadToken: downloadToken,
quotesMessageId: quotesMessageId, quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender, isDeletedFromSender: isDeletedFromSender,
@ -8272,6 +8381,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<bool> mediaStored = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(), Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(), Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(), Value<bool> isDeletedFromSender = const Value.absent(),
@ -8290,6 +8400,7 @@ class $$MessagesTableTableManager extends RootTableManager<
senderId: senderId, senderId: senderId,
content: content, content: content,
mediaId: mediaId, mediaId: mediaId,
mediaStored: mediaStored,
downloadToken: downloadToken, downloadToken: downloadToken,
quotesMessageId: quotesMessageId, quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender, isDeletedFromSender: isDeletedFromSender,

View file

@ -24,7 +24,6 @@ import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart
as server; as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; 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/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/messages.dart';
import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
@ -94,7 +93,6 @@ class ApiService {
if (!globalIsAppInBackground) { if (!globalIsAppInBackground) {
unawaited(retransmitRawBytes()); unawaited(retransmitRawBytes());
unawaited(tryTransmitMessages()); unawaited(tryTransmitMessages());
unawaited(retryMediaUpload(false));
unawaited(tryDownloadAllMediaFiles()); unawaited(tryDownloadAllMediaFiles());
unawaited(notifyContactsAboutProfileChange()); unawaited(notifyContactsAboutProfileChange());
twonlyDB.markUpdated(); twonlyDB.markUpdated();

View file

@ -1,8 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart'; 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/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.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/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.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);
}

View file

@ -1,124 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.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/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.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/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.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/log.dart';
import 'package:twonly/src/utils/misc.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( Future<MediaFileService?> initializeMediaUpload(
MediaType type, MediaType type,
@ -141,78 +38,10 @@ Future<MediaFileService?> initializeMediaUpload(
return MediaFileService.fromMedia(mediaFile); return MediaFileService.fromMedia(mediaFile);
} }
Future<void> handlePreProcessingState(MediaUpload media) async { Future<void> insertMediaFileInMessagesTable(
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(
MediaFileService mediaService, MediaFileService mediaService,
List<String> groupIds, List<String> groupIds,
) async { ) async {
final messageIds = <Message>[];
for (final groupId in groupIds) { for (final groupId in groupIds) {
final message = await twonlyDB.messagesDao.insertMessage( final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
@ -221,7 +50,6 @@ Future<void> finalizeUpload(
), ),
); );
if (message != null) { if (message != null) {
messageIds.add(message);
// de-archive contact when sending a new message // de-archive contact when sending a new message
await twonlyDB.groupsDao.updateGroup( await twonlyDB.groupsDao.updateGroup(
message.groupId, message.groupId,
@ -234,233 +62,131 @@ Future<void> finalizeUpload(
} }
} }
unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId)); unawaited(startBackgroundMediaUpload(mediaService));
} }
final lockingHandleNextMediaUploadStep = Mutex(); Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
Future<void> handleNextMediaUploadSteps(String mediaUploadId) async { if (mediaService.mediaFile.uploadState == UploadState.initialized) {
await lockingHandleNextMediaUploadStep.protect(() async { await mediaService.setUploadState(UploadState.preprocessing);
final mediaUpload = await twonlyDB.mediaUploadsDao if (!mediaService.tempPath.existsSync()) {
.getMediaUploadById(mediaUploadId) await mediaService.compressMedia();
.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;
} }
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) { if (!mediaService.encryptedPath.existsSync()) {
/// the finalize function was not called yet... await _encryptMediaFiles(mediaService);
return false;
}
await handleMediaUpload(mediaUpload);
} catch (e) {
Log.error('Non recoverable error while sending media file: $e');
await handleUploadError(mediaUpload);
} }
return false;
}); if (!mediaService.uploadRequestPath.existsSync()) {
await _createUploadRequest(mediaService);
}
await mediaService.setUploadState(UploadState.uploading);
}
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
await _uploadUploadRequest(mediaService);
}
} }
/// Future<void> _encryptMediaFiles(MediaFileService mediaService) async {
/// -- private functions -- /// if there is a video wait until it is finished with compression
///
///
///
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async { final dataToEncrypt = await mediaService.tempPath.readAsBytes();
var failed = false;
final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', ''));
final media = await twonlyDB.mediaUploadsDao final chacha20 = FlutterChacha20.poly1305Aead();
.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}',
);
}
}
if (failed) { final secretBox = await chacha20.encrypt(
for (final messageId in media.messageIds!) { dataToEncrypt,
await twonlyDB.messagesDao.updateMessageByMessageId( secretKey: SecretKey(mediaService.mediaFile.encryptionKey!),
messageId, nonce: mediaService.mediaFile.encryptionNonce,
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),
),
); );
for (final messageId in media.messageIds!) { await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes));
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId, mediaService.encryptedPath
const MessagesCompanion( .writeAsBytesSync(Uint8List.fromList(secretBox.cipherText));
acknowledgeByServer: Value(true),
errorWhileSending: Value(false), await mediaService.setUploadState(UploadState.uploading);
),
);
}
} }
Future<void> handleUploadError(MediaUpload mediaUpload) async { Future<void> _createUploadRequest(MediaFileService media) 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!;
final downloadTokens = <Uint8List>[]; final downloadTokens = <Uint8List>[];
final messagesOnSuccess = <TextMessage>[]; final messagesOnSuccess = <TextMessage>[];
for (var i = 0; i < messageIds.length; i++) { final messages =
final message = await twonlyDB.messagesDao await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId);
.getMessageByMessageId(messageIds[i])
.getSingleOrNull();
if (message == null) continue;
if (message.downloadState == DownloadState.downloaded) { for (final message in messages) {
// only upload message which are not yet uploaded (or in case of an error re-uploaded) final groupMembers =
continue; 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); await twonlyDB.contactsDao.incFlameCounter(
groupMember.contactId,
final msg = MessageJson( false,
kind: MessageKind.media, message.createdAt,
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.messagesDao.updateMessageByMessageId(
message.messageId, final downloadToken = getRandomUint8List(32);
const MessagesCompanion(errorWhileSending: Value(true)),
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( final uploadRequest = UploadRequest(
messagesOnSuccess: messagesOnSuccess, messagesOnSuccess: messagesOnSuccess,
downloadTokens: downloadTokens, downloadTokens: downloadTokens,
@ -469,6 +195,10 @@ Future<void> handleMediaUpload(MediaUpload media) async {
final uploadRequestBytes = uploadRequest.writeToBuffer(); final uploadRequestBytes = uploadRequest.writeToBuffer();
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
}
Future<void> _uploadUploadRequest(MediaFileService media) async {
final apiAuthTokenRaw = await const FlutterSecureStorage() final apiAuthTokenRaw = await const FlutterSecureStorage()
.read(key: SecureStorageKeys.apiAuthToken); .read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthTokenRaw == null) { if (apiAuthTokenRaw == null) {
@ -477,108 +207,27 @@ Future<void> handleMediaUpload(MediaUpload media) async {
} }
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw)); final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
final uploadRequestFile = await writeSendMediaFile(
media.mediaUploadId,
'upload',
uploadRequestBytes,
);
final apiUrl = final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload'; 'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
try { // try {
Log.info('Starting upload from ${media.mediaUploadId}'); Log.info('Starting upload from ${media.mediaFile.mediaId}');
final task = UploadTask.fromFile( final task = UploadTask.fromFile(
taskId: 'upload_${media.mediaUploadId}', taskId: 'upload_${media.mediaFile.mediaId}',
displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video', displayName: media.mediaFile.type.name,
file: uploadRequestFile, file: media.uploadRequestPath,
url: apiUrl, url: apiUrl,
priority: 0, priority: 0,
retries: 10, retries: 10,
headers: { headers: {
'x-twonly-auth-token': apiAuthToken, '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 response = await requestMultipart.send(); Log.info('Enqueue upload task: ${task.taskId}');
if (response.statusCode == 200) {
Log.info('Upload successful!'); await FileDownloader().enqueue(task);
await handleUploadSuccess(media);
return; await media.setUploadState(UploadState.backgroundUploadTaskStarted);
} 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}');
}
} }

View file

@ -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, String? receiptId,
Receipt? receipt, Receipt? receipt,
bool reupload = false, bool reupload = false,
bool onlyReturnEncryptedData = false,
}) async { }) async {
try { try {
if (receiptId == null && receipt == null) return; if (receiptId == null && receipt == null) return null;
if (receipt == null) { if (receipt == null) {
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) { if (receipt == null) {
Log.error('Receipt $receiptId not found.'); Log.error('Receipt $receiptId not found.');
return; return null;
} }
} }
receiptId = receipt.receiptId; 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!'); Log.error('$receiptId message already uploaded!');
return; return null;
} }
Log.info('Uploading $receiptId (Message to ${receipt.contactId})'); Log.info('Uploading $receiptId (Message to ${receipt.contactId})');
@ -86,7 +88,7 @@ Future<void> tryToSendCompleteMessage({
); );
if (cipherText == null) { if (cipherText == null) {
Log.error('Could not encrypt the message. Aborting and trying again.'); Log.error('Could not encrypt the message. Aborting and trying again.');
return; return null;
} }
message.encryptedContent = cipherText.serialize(); message.encryptedContent = cipherText.serialize();
switch (cipherText.getType()) { switch (cipherText.getType()) {
@ -96,10 +98,14 @@ Future<void> tryToSendCompleteMessage({
message.type = pb.Message_Type.CIPHERTEXT; message.type = pb.Message_Type.CIPHERTEXT;
default: default:
Log.error('Invalid ciphertext type: ${cipherText.getType()}.'); Log.error('Invalid ciphertext type: ${cipherText.getType()}.');
return; return null;
} }
} }
if (onlyReturnEncryptedData) {
return (message.writeToBuffer(), pushData);
}
final resp = await apiService.sendTextMessage( final resp = await apiService.sendTextMessage(
receipt.contactId, receipt.contactId,
message.writeToBuffer(), message.writeToBuffer(),
@ -114,7 +120,7 @@ Future<void> tryToSendCompleteMessage({
receipt.contactId, receipt.contactId,
const ContactsCompanion(deleted: Value(true)), const ContactsCompanion(deleted: Value(true)),
); );
return; return null;
} }
} }
@ -149,12 +155,14 @@ Future<void> tryToSendCompleteMessage({
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
} }
} }
return null;
} }
Future<void> sendCipherText( Future<(Uint8List, Uint8List?)?> sendCipherText(
int contactId, int contactId,
pb.EncryptedContent encryptedContent, pb.EncryptedContent encryptedContent, {
) async { bool onlyReturnEncryptedData = false,
}) async {
final response = pb.Message() final response = pb.Message()
..type = pb.Message_Type.CIPHERTEXT ..type = pb.Message_Type.CIPHERTEXT
..encryptedContent = encryptedContent.writeToBuffer(); ..encryptedContent = encryptedContent.writeToBuffer();
@ -163,12 +171,17 @@ Future<void> sendCipherText(
ReceiptsCompanion( ReceiptsCompanion(
contactId: Value(contactId), contactId: Value(contactId),
message: Value(response.writeToBuffer()), message: Value(response.writeToBuffer()),
ackByServerAt: Value(onlyReturnEncryptedData ? DateTime.now() : null),
), ),
); );
if (receipt != null) { if (receipt != null) {
await tryToSendCompleteMessage(receipt: receipt); return tryToSendCompleteMessage(
receipt: receipt,
onlyReturnEncryptedData: onlyReturnEncryptedData,
);
} }
return null;
} }
Future<void> notifyContactAboutOpeningMessage( Future<void> notifyContactAboutOpeningMessage(

View file

@ -49,6 +49,26 @@ class MediaFileService {
await updateFromDB(); 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 { Future<void> setRequiresAuth(bool requiresAuthentication) async {
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId, mediaFile.mediaId,
@ -98,7 +118,8 @@ class MediaFileService {
encryptedPath, encryptedPath,
originalPath, originalPath,
storedPath, storedPath,
thumbnailPath thumbnailPath,
uploadRequestPath
]; ];
for (final path in pathsToRemove) { for (final path in pathsToRemove) {
@ -160,6 +181,10 @@ class MediaFileService {
'tmp', 'tmp',
namePrefix: '.encrypted', namePrefix: '.encrypted',
); );
File get uploadRequestPath => _buildFilePath(
'tmp',
namePrefix: '.upload',
);
File get originalPath => _buildFilePath( File get originalPath => _buildFilePath(
'tmp', 'tmp',
namePrefix: '.original', namePrefix: '.original',

View file

@ -8,12 +8,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/constants/secure_storage_keys.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.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/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'
show gMediaShowInfinite;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();
@ -34,7 +31,7 @@ Future<void> customLocalPushNotification(String title, String msg) async {
); );
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
gMediaShowInfinite + Random.secure().nextInt(9999), Random.secure().nextInt(9999),
title, title,
msg, msg,
notificationDetails, notificationDetails,

View file

@ -4,9 +4,9 @@ import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart'; import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/src/model/json/userdata.dart'; 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/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.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/utils/storage.dart';
Future<void> enableTwonlySafe(String password) async { Future<void> enableTwonlySafe(String password) async {

View file

@ -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/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.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/services/twonly_safe/common.twonly_safe.dart';
import 'package:twonly/src/utils/log.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/utils/storage.dart';
import 'package:twonly/src/views/settings/backup/backup.view.dart'; import 'package:twonly/src/views/settings/backup/backup.view.dart';

View file

@ -1,30 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; 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:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path/path.dart'; import 'package:twonly/globals.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/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class SaveToGalleryButton extends StatefulWidget { class SaveToGalleryButton extends StatefulWidget {
const SaveToGalleryButton({ const SaveToGalleryButton({
required this.getMergedImage, required this.getMergedImage,
required this.isLoading, required this.isLoading,
required this.displayButtonLabel, required this.displayButtonLabel,
required this.mediaService,
super.key, super.key,
this.mediaUploadId,
this.videoFilePath,
}); });
final Future<Uint8List?> Function() getMergedImage; final Future<Uint8List?> Function() getMergedImage;
final bool displayButtonLabel; final bool displayButtonLabel;
final File? videoFilePath; final MediaFileService mediaService;
final int? mediaUploadId;
final bool isLoading; final bool isLoading;
@override @override
@ -54,44 +46,20 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
}); });
String? res; String? res;
var memoryPath = await getMediaBaseFilePath('memories');
if (widget.mediaUploadId != null) { final storedMediaPath = widget.mediaService.storedPath;
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;
if (widget.videoFilePath != null) { final storeToGallery = gUser.storeMediaFilesInGallery;
memoryPath += '.mp4';
await File(widget.videoFilePath!.path).copy(memoryPath); await widget.mediaService.storeMediaFile();
unawaited(createThumbnailsForVideo(File(memoryPath)));
if (storeToGallery) { if (storeToGallery) {
res = await saveVideoToGallery(widget.videoFilePath!.path); res = await saveVideoToGallery(storedMediaPath.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);
}
} }
await widget.mediaService.compressMedia();
await widget.mediaService.createThumbnail();
if (res == null) { if (res == null) {
setState(() { setState(() {
_imageSaved = true; _imageSaved = true;

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:permission_handler/permission_handler.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.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/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
@ -92,9 +90,9 @@ class CameraPreviewControllerView extends StatelessWidget {
required this.selectedCameraDetails, required this.selectedCameraDetails,
required this.screenshotController, required this.screenshotController,
super.key, super.key,
this.sendTo, this.sendToGroup,
}); });
final Contact? sendTo; final Group? sendToGroup;
final Future<CameraController?> Function( final Future<CameraController?> Function(
int sCameraId, int sCameraId,
bool init, bool init,
@ -112,7 +110,7 @@ class CameraPreviewControllerView extends StatelessWidget {
if (snap.hasData) { if (snap.hasData) {
if (snap.data!) { if (snap.data!) {
return CameraPreviewView( return CameraPreviewView(
sendTo: sendTo, sendToGroup: sendToGroup,
selectCamera: selectCamera, selectCamera: selectCamera,
cameraController: cameraController, cameraController: cameraController,
selectedCameraDetails: selectedCameraDetails, selectedCameraDetails: selectedCameraDetails,
@ -141,9 +139,9 @@ class CameraPreviewView extends StatefulWidget {
required this.selectedCameraDetails, required this.selectedCameraDetails,
required this.screenshotController, required this.screenshotController,
super.key, super.key,
this.sendTo, this.sendToGroup,
}); });
final Contact? sendTo; final Group? sendToGroup;
final Future<CameraController?> Function( final Future<CameraController?> Function(
int sCameraId, int sCameraId,
bool init, bool init,
@ -328,7 +326,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
pageBuilder: (context, a1, a2) => ShareImageEditorView( pageBuilder: (context, a1, a2) => ShareImageEditorView(
imageBytesFuture: imageBytes, imageBytesFuture: imageBytes,
sharedFromGallery: sharedFromGallery, sharedFromGallery: sharedFromGallery,
sendTo: widget.sendTo, sendToGroup: widget.sendToGroup,
mediaFileService: mediaFileService, mediaFileService: mediaFileService,
), ),
transitionsBuilder: (context, animation, secondaryAnimation, child) { transitionsBuilder: (context, animation, secondaryAnimation, child) {
@ -347,7 +345,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
if (!mounted) return true; if (!mounted) return true;
// shouldReturn is null when the user used the back button // shouldReturn is null when the user used the back button
if (shouldReturn != null && shouldReturn) { if (shouldReturn != null && shouldReturn) {
if (widget.sendTo == null) { if (widget.sendToGroup == null) {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
} else { } else {
Navigator.pop(context); Navigator.pop(context);
@ -476,19 +474,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
}); });
try { try {
File? videoPathFile;
final videoPath = await widget.cameraController?.stopVideoRecording(); final videoPath = await widget.cameraController?.stopVideoRecording();
if (videoPath != null) { if (videoPath == null) return;
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);
}
}
await widget.cameraController?.pausePreview(); await widget.cameraController?.pausePreview();
if (await pushMediaEditor(null, videoPathFile)) { if (await pushMediaEditor(null, File(videoPath.path))) {
return; return;
} }
} on CameraException catch (e) { } on CameraException catch (e) {
@ -568,9 +557,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
), ),
), ),
if (!sharePreviewIsShown && if (!sharePreviewIsShown &&
widget.sendTo != null && widget.sendToGroup != null &&
!isVideoRecording) !isVideoRecording)
SendToWidget(sendTo: getContactDisplayName(widget.sendTo!)), SendToWidget(sendTo: widget.sendToGroup!.groupName),
if (!sharePreviewIsShown && !isVideoRecording) if (!sharePreviewIsShown && !isVideoRecording)
Positioned( Positioned(
right: 5, right: 5,
@ -722,7 +711,7 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
videoRecordingStarted: videoRecordingStarted, videoRecordingStarted: videoRecordingStarted,
maxVideoRecordingTime: maxVideoRecordingTime, maxVideoRecordingTime: maxVideoRecordingTime,
), ),
if (!sharePreviewIsShown && widget.sendTo != null) if (!sharePreviewIsShown && widget.sendToGroup != null)
Positioned( Positioned(
left: 5, left: 5,
top: 10, top: 10,

View file

@ -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'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart';
class CameraSendToView extends StatefulWidget { class CameraSendToView extends StatefulWidget {
const CameraSendToView(this.sendTo, {super.key}); const CameraSendToView(this.sendToGroup, {super.key});
final Contact sendTo; final Group sendToGroup;
@override @override
State<CameraSendToView> createState() => CameraSendToViewState(); State<CameraSendToView> createState() => CameraSendToViewState();
} }
@ -77,7 +77,7 @@ class CameraSendToViewState extends State<CameraSendToView> {
), ),
CameraPreviewControllerView( CameraPreviewControllerView(
selectCamera: selectCamera, selectCamera: selectCamera,
sendTo: widget.sendTo, sendToGroup: widget.sendToGroup,
cameraController: cameraController, cameraController: cameraController,
selectedCameraDetails: selectedCameraDetails, selectedCameraDetails: selectedCameraDetails,
screenshotController: screenshotController, screenshotController: screenshotController,

View file

@ -2,16 +2,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:screenshot/screenshot.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/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.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/camera/share_image_view.dart';
import 'package:twonly/src/views/components/media_view_sizing.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/components/notification_badge.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
List<Layer> layers = []; List<Layer> layers = [];
List<Layer> undoLayers = []; List<Layer> undoLayers = [];
List<Layer> removedLayers = []; List<Layer> removedLayers = [];
const gMediaShowInfinite = 999999;
class ShareImageEditorView extends StatefulWidget { class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({ const ShareImageEditorView({
required this.sharedFromGallery, required this.sharedFromGallery,
required this.mediaFileService, required this.mediaFileService,
super.key, super.key,
this.imageBytesFuture, this.imageBytesFuture,
this.sendTo, this.sendToGroup,
}); });
final Future<Uint8List?>? imageBytesFuture; final Future<Uint8List?>? imageBytesFuture;
final Group? sendTo; final Group? sendToGroup;
final bool sharedFromGallery; final bool sharedFromGallery;
final MediaFileService mediaFileService; final MediaFileService mediaFileService;
@override @override
@ -66,9 +58,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
ImageItem currentImage = ImageItem(); ImageItem currentImage = ImageItem();
ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
/// Media upload variables
Future<bool>? videoUploadHandler;
MediaFileService get mediaService => widget.mediaFileService; MediaFileService get mediaService => widget.mediaFileService;
MediaFile get media => widget.mediaFileService.mediaFile; MediaFile get media => widget.mediaFileService.mediaFile;
@ -78,8 +67,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
layers.add(FilterLayerData()); layers.add(FilterLayerData());
if (widget.sendTo != null) { if (widget.sendToGroup != null) {
selectedGroupIds.add(widget.sendTo!.groupId); selectedGroupIds.add(widget.sendToGroup!.groupId);
} }
if (widget.imageBytesFuture != null) { if (widget.imageBytesFuture != null) {
@ -284,17 +273,18 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
Future<void> pushShareImageView() async { Future<void> pushShareImageView() async {
final imageBytes = storeImageAsOriginal(); final mediaStoreFuture =
(media.type == MediaType.image) ? storeImageAsOriginal() : null;
await videoController?.pause(); await videoController?.pause();
if (isDisposed || !mounted) return; if (isDisposed || !mounted) return;
final wasSend = await Navigator.push( final wasSend = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
imageBytesFuture: imageBytes, selectedGroupIds: selectedGroupIds,
selectedUserIds: selectedGroupIds, updateSelectedGroupIds: updateSelectedGroupIds,
updateStatus: updateSelectedGroupIds, mediaStoreFuture: mediaStoreFuture,
videoUploadHandler: videoUploadHandler,
mediaFileService: mediaService, 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.length == 1) {
if (layers.first is BackgroundLayerData) { if (layers.first is BackgroundLayerData) {
final image = (layers.first as BackgroundLayerData).image.bytes; 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) { if (image == null) {
Log.error('screenshotController did not return image bytes'); Log.error('screenshotController did not return image bytes');
return; return null;
}
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);
} }
for (final x in layers) { for (final x in layers) {
x.showCustomButtons = true; x.showCustomButtons = true;
} }
setState(() {}); 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 { Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
@ -377,14 +374,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
if (!context.mounted) return; if (!context.mounted) return;
// first finalize the upload // Insert media file into the messages database and start uploading process in the background
await finalizeUpload(mediaService, [widget.sendTo!.groupId]); await insertMediaFileInMessagesTable(
mediaService,
/// then call the upload process in the background [widget.sendToGroup!.groupId],
await encryptMediaFiles(
mediaUploadId!,
imageHandler,
videoUploadHandler,
); );
if (context.mounted) { if (context.mounted) {
@ -434,14 +427,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
SaveToGalleryButton( SaveToGalleryButton(
getMergedImage: getMergedImage, getMergedImage: getEditedImageBytes,
mediaUploadId: mediaUploadId, mediaService: mediaService,
videoFilePath: widget.videoFilePath, displayButtonLabel: widget.sendToGroup == null,
displayButtonLabel: widget.sendTo == null,
isLoading: loadingImage, isLoading: loadingImage,
), ),
if (widget.sendTo != null) const SizedBox(width: 10), if (widget.sendToGroup != null) const SizedBox(width: 10),
if (widget.sendTo != null) if (widget.sendToGroup != null)
OutlinedButton( OutlinedButton(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
iconColor: Theme.of(context).colorScheme.primary, iconColor: Theme.of(context).colorScheme.primary,
@ -451,7 +443,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: pushShareImageView, onPressed: pushShareImageView,
child: const FaIcon(FontAwesomeIcons.userPlus), child: const FaIcon(FontAwesomeIcons.userPlus),
), ),
SizedBox(width: widget.sendTo == null ? 20 : 10), SizedBox(width: widget.sendToGroup == null ? 20 : 10),
FilledButton.icon( FilledButton.icon(
icon: sendingOrLoadingImage icon: sendingOrLoadingImage
? SizedBox( ? SizedBox(
@ -467,7 +459,8 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
: const FaIcon(FontAwesomeIcons.solidPaperPlane), : const FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (sendingOrLoadingImage) return; if (sendingOrLoadingImage) return;
if (widget.sendTo == null) return pushShareImageView(); if (widget.sendToGroup == null)
return pushShareImageView();
await sendImageToSinglePerson(); await sendImageToSinglePerson();
}, },
style: ButtonStyle( style: ButtonStyle(
@ -479,9 +472,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
), ),
label: Text( label: Text(
(widget.sendTo == null) (widget.sendToGroup == null)
? context.lang.shareImagedEditorShareWith ? context.lang.shareImagedEditorShareWith
: getContactDisplayName(widget.sendTo!), : widget.sendToGroup!.groupName,
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
), ),

View file

@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.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/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.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/mediafiles/mediafile.service.dart';
@ -21,19 +22,15 @@ import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
const ShareImageView({ const ShareImageView({
required this.imageBytesFuture, required this.selectedGroupIds,
required this.selectedUserIds, required this.updateSelectedGroupIds,
required this.updateStatus, required this.mediaStoreFuture,
required this.videoUploadHandler,
required this.mediaFileService, required this.mediaFileService,
super.key, super.key,
this.enableVideoAudio,
}); });
final Future<Uint8List?> imageBytesFuture; final HashSet<String> selectedGroupIds;
final HashSet<int> selectedUserIds; final void Function(String, bool) updateSelectedGroupIds;
final bool? enableVideoAudio; final Future<bool>? mediaStoreFuture;
final void Function(int, bool) updateStatus;
final Future<bool>? videoUploadHandler;
final MediaFileService mediaFileService; final MediaFileService mediaFileService;
@override @override
@ -69,17 +66,11 @@ class _ShareImageView extends State<ShareImageView> {
} }
Future<void> initAsync() async { Future<void> initAsync() async {
imageBytes = await widget.imageBytesFuture; if (widget.mediaStoreFuture != null) {
if (imageBytes != null) { await widget.mediaStoreFuture;
final imageHandler =
addOrModifyImageToUpload(widget.mediaUploadId, imageBytes!);
// start with the pre upload of the media file...
await encryptMediaFiles(
widget.mediaUploadId,
imageHandler,
widget.videoUploadHandler,
);
} }
await widget.mediaFileService.setUploadState(UploadState.preprocessing);
unawaited(startBackgroundMediaUpload(widget.mediaFileService));
if (!mounted) return; if (!mounted) return;
setState(() {}); setState(() {});
} }