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

View file

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

View file

@ -285,6 +285,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
.write(updatedValues);
}
Future<void> updateMessagesByMediaId(
String mediaId,
MessagesCompanion updatedValues,
) {
return (update(messages)..where((c) => c.mediaId.equals(mediaId)))
.write(updatedValues);
}
Future<Message?> insertMessage(MessagesCompanion message) async {
try {
final rowId = await into(messages).insert(message);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,13 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
@ -48,3 +53,59 @@ Future<void> initFileDownloader() async {
);
}
}
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
final mediaId = update.task.taskId.replaceAll('upload_', '');
final media = await twonlyDB.mediaFilesDao.getMediaFileById(mediaId);
if (media == null) {
Log.error(
'Got an upload task but no upload media in the media upload database',
);
return;
}
if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) {
Log.info('Upload of ${media.mediaId} success!');
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploaded),
),
);
await twonlyDB.messagesDao.updateMessagesByMediaId(
media.mediaId,
const MessagesCompanion(
ackByServer: Value(true),
),
);
return;
}
Log.error(
'Got HTTP error ${update.responseStatusCode} for $mediaId',
);
if (update.responseStatusCode == 429) {
await twonlyDB.mediaFilesDao.updateMedia(
mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploadLimitReached),
),
);
return;
}
}
Log.info(
'Background upload failed for $mediaId with status ${update.status}. Trying again.',
);
final mediaService = await MediaFileService.fromMedia(media);
await mediaService.setUploadState(UploadState.uploading);
// In all other cases just try the upload again...
await startBackgroundMediaUpload(mediaService);
}

View file

@ -1,124 +1,21 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'package:mutex/mutex.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart';
import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:video_compress/video_compress.dart';
/// States:
/// when user recorded an video
/// 1. Compress video
/// when user clicked the send button (direct send) or share with
/// 2. Encrypt media files
/// 3. Upload media files
/// click send button
/// 4. Finalize upload by websocket -> get download tokens
/// 5. Send all users the message
/// Create a new entry in the database
// Future<bool> checkForFailedUploads() async {
// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
// final mediaUploadIds = <int>[];
// for (final message in messages) {
// if (mediaUploadIds.contains(message.mediaUploadId)) {
// continue;
// }
// final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
// message.mediaUploadId!,
// const MediaUploadsCompanion(
// state: Value(UploadState.pending),
// encryptionData: Value(
// null, // start from scratch e.q. encrypt the files again if already happen
// ),
// ),
// );
// if (affectedRows == 0) {
// Log.error(
// 'The media from message ${message.messageId} already deleted.',
// );
// await twonlyDB.messagesDao.updateMessageByMessageId(
// message.messageId,
// const MessagesCompanion(
// errorWhileSending: Value(true),
// ),
// );
// } else {
// mediaUploadIds.add(message.mediaUploadId!);
// }
// }
// if (messages.isNotEmpty) {
// Log.error(
// 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.',
// );
// }
// return mediaUploadIds.isNotEmpty; // return true if there are affected
// }
final lockingHandleMediaFile = Mutex();
Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
if (maxRetries == 0) {
Log.error('retried media upload 3 times. abort retrying');
return;
}
final retry = await lockingHandleMediaFile.protect<bool>(() async {
final mediaFiles = await twonlyDB.mediaUploadsDao.getMediaUploadsForRetry();
if (mediaFiles.isEmpty) {
return checkForFailedUploads();
}
Log.info('re uploading ${mediaFiles.length} media files.');
for (final mediaFile in mediaFiles) {
if (mediaFile.messageIds == null || mediaFile.metadata == null) {
if (appRestarted) {
/// When the app got restarted and the messageIds or the metadata is not
/// set then the app was closed before the images was send.
await twonlyDB.mediaUploadsDao
.deleteMediaUpload(mediaFile.mediaUploadId);
Log.info(
'upload can be removed, the finalized function was never called...',
);
}
continue;
}
if (mediaFile.state == UploadState.readyToUpload) {
await handleNextMediaUploadSteps(mediaFile.mediaUploadId);
} else {
await handlePreProcessingState(mediaFile);
}
}
return false;
});
if (retry) {
await retryMediaUpload(false, maxRetries: maxRetries - 1);
}
}
Future<MediaFileService?> initializeMediaUpload(
MediaType type,
@ -141,78 +38,10 @@ Future<MediaFileService?> initializeMediaUpload(
return MediaFileService.fromMedia(mediaFile);
}
Future<void> handlePreProcessingState(MediaUpload media) async {
try {
final imageHandler = readSendMediaFile(media.mediaUploadId, 'png');
final videoHandler = compressVideoIfExists(media.mediaUploadId);
await encryptMediaFiles(
media.mediaUploadId,
imageHandler,
videoHandler,
);
} catch (e) {
Log.error('${media.mediaUploadId} got error in pre processing: $e');
await handleUploadError(media);
}
}
Future<void> encryptMediaFiles(
int mediaUploadId,
Future<Uint8List> imageHandler,
Future<bool>? videoHandler,
) async {
Log.info('$mediaUploadId encrypting files');
var dataToEncrypt = await imageHandler;
/// if there is a video wait until it is finished with compression
if (videoHandler != null) {
if (await videoHandler) {
final compressedVideo = await readSendMediaFile(mediaUploadId, 'mp4');
dataToEncrypt = combineUint8Lists(dataToEncrypt, compressedVideo);
}
}
final state = MediaEncryptionData();
final chacha20 = FlutterChacha20.poly1305Aead();
state
..encryptionKey = secretKey.bytes
..encryptionNonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt(
dataToEncrypt,
secretKey: secretKey,
nonce: state.encryptionNonce,
);
state
..encryptionMac = secretBox.mac.bytes
..sha2Hash = (await Sha256().hash(secretBox.cipherText)).bytes;
final encryptedBytes = Uint8List.fromList(secretBox.cipherText);
await writeSendMediaFile(
mediaUploadId,
'encrypted',
encryptedBytes,
);
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
MediaUploadsCompanion(
state: const Value(UploadState.readyToUpload),
encryptionData: Value(state),
),
);
unawaited(handleNextMediaUploadSteps(mediaUploadId));
}
Future<void> finalizeUpload(
Future<void> insertMediaFileInMessagesTable(
MediaFileService mediaService,
List<String> groupIds,
) async {
final messageIds = <Message>[];
for (final groupId in groupIds) {
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(
@ -221,7 +50,6 @@ Future<void> finalizeUpload(
),
);
if (message != null) {
messageIds.add(message);
// de-archive contact when sending a new message
await twonlyDB.groupsDao.updateGroup(
message.groupId,
@ -234,233 +62,131 @@ Future<void> finalizeUpload(
}
}
unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId));
unawaited(startBackgroundMediaUpload(mediaService));
}
final lockingHandleNextMediaUploadStep = Mutex();
Future<void> handleNextMediaUploadSteps(String mediaUploadId) async {
await lockingHandleNextMediaUploadStep.protect(() async {
final mediaUpload = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
.getSingleOrNull();
if (mediaUpload == null) return false;
if (mediaUpload.state == UploadState.receiverNotified ||
mediaUpload.state == UploadState.uploadTaskStarted) {
/// Upload done and all users are notified :)
Log.info('$mediaUploadId is already done');
return false;
Future<void> startBackgroundMediaUpload(MediaFileService mediaService) async {
if (mediaService.mediaFile.uploadState == UploadState.initialized) {
await mediaService.setUploadState(UploadState.preprocessing);
if (!mediaService.tempPath.existsSync()) {
await mediaService.compressMedia();
}
try {
/// Stage 1: media files are not yet encrypted...
if (mediaUpload.encryptionData == null) {
// when set this function will be called again by encryptAndPreUploadMediaFiles...
return false;
}
if (mediaUpload.messageIds == null || mediaUpload.metadata == null) {
/// the finalize function was not called yet...
return false;
}
await handleMediaUpload(mediaUpload);
} catch (e) {
Log.error('Non recoverable error while sending media file: $e');
await handleUploadError(mediaUpload);
if (!mediaService.encryptedPath.existsSync()) {
await _encryptMediaFiles(mediaService);
}
return false;
});
if (!mediaService.uploadRequestPath.existsSync()) {
await _createUploadRequest(mediaService);
}
await mediaService.setUploadState(UploadState.uploading);
}
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
await _uploadUploadRequest(mediaService);
}
}
///
/// -- private functions --
///
///
///
Future<void> _encryptMediaFiles(MediaFileService mediaService) async {
/// if there is a video wait until it is finished with compression
Future<void> handleUploadStatusUpdate(TaskStatusUpdate update) async {
var failed = false;
final mediaUploadId = int.parse(update.task.taskId.replaceAll('upload_', ''));
final dataToEncrypt = await mediaService.tempPath.readAsBytes();
final media = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId)
.getSingleOrNull();
if (media == null) {
Log.error(
'Got an upload task but no upload media in the media upload database',
);
return;
}
if (update.status == TaskStatus.failed ||
update.status == TaskStatus.canceled) {
Log.error('Upload failed: ${update.status}');
failed = true;
} else if (update.status == TaskStatus.complete) {
if (update.responseStatusCode == 200) {
await handleUploadSuccess(media);
return;
} else if (update.responseStatusCode != null) {
if (update.responseStatusCode! >= 400 &&
update.responseStatusCode! < 500) {
failed = true;
}
Log.error(
'Got error while uploading: ${update.responseStatusCode}',
);
}
}
final chacha20 = FlutterChacha20.poly1305Aead();
if (failed) {
for (final messageId in media.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId,
const MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(true),
),
);
}
}
Log.info(
'Status update for ${update.task.taskId} with status ${update.status}',
);
}
Future<void> handleUploadSuccess(MediaUpload media) async {
Log.info('Upload of ${media.mediaUploadId} success!');
currentUploadTasks.remove(media.mediaUploadId);
await twonlyDB.mediaUploadsDao.updateMediaUpload(
media.mediaUploadId,
const MediaUploadsCompanion(
state: Value(UploadState.receiverNotified),
),
final secretBox = await chacha20.encrypt(
dataToEncrypt,
secretKey: SecretKey(mediaService.mediaFile.encryptionKey!),
nonce: mediaService.mediaFile.encryptionNonce,
);
for (final messageId in media.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId,
const MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(false),
),
);
}
await mediaService.setEncryptedMac(Uint8List.fromList(secretBox.mac.bytes));
mediaService.encryptedPath
.writeAsBytesSync(Uint8List.fromList(secretBox.cipherText));
await mediaService.setUploadState(UploadState.uploading);
}
Future<void> handleUploadError(MediaUpload mediaUpload) async {
// if the messageIds are already there notify the user about this error...
if (mediaUpload.messageIds != null) {
for (final messageId in mediaUpload.messageIds!) {
await twonlyDB.messagesDao.updateMessageByMessageId(
messageId,
const MessagesCompanion(
errorWhileSending: Value(true),
),
);
}
}
await twonlyDB.mediaUploadsDao.deleteMediaUpload(mediaUpload.mediaUploadId);
}
Future<void> handleMediaUpload(MediaUpload media) async {
final bytesToUpload =
await readSendMediaFile(media.mediaUploadId, 'encrypted');
if (media.messageIds == null) return;
final messageIds = media.messageIds!;
Future<void> _createUploadRequest(MediaFileService media) async {
final downloadTokens = <Uint8List>[];
final messagesOnSuccess = <TextMessage>[];
for (var i = 0; i < messageIds.length; i++) {
final message = await twonlyDB.messagesDao
.getMessageByMessageId(messageIds[i])
.getSingleOrNull();
if (message == null) continue;
final messages =
await twonlyDB.messagesDao.getMessagesByMediaId(media.mediaFile.mediaId);
if (message.downloadState == DownloadState.downloaded) {
// only upload message which are not yet uploaded (or in case of an error re-uploaded)
continue;
}
for (final message in messages) {
final groupMembers =
await twonlyDB.groupsDao.getGroupMembers(message.groupId);
for (final groupMember in groupMembers) {
/// only send the upload to the users
if (media.mediaFile.reuploadRequestedBy != null) {
if (!media.mediaFile.reuploadRequestedBy!
.contains(groupMember.contactId)) {
continue;
}
}
final downloadToken = getRandomUint8List(32);
final msg = MessageJson(
kind: MessageKind.media,
messageSenderId: messageIds[i],
content: MediaMessageContent(
downloadToken: downloadToken,
maxShowTime: media.metadata!.maxShowTime,
isRealTwonly: media.metadata!.isRealTwonly,
isVideo: media.metadata!.isVideo,
mirrorVideo: media.metadata!.mirrorVideo,
encryptionKey: media.encryptionData!.encryptionKey,
encryptionMac: media.encryptionData!.encryptionMac,
encryptionNonce: media.encryptionData!.encryptionNonce,
),
timestamp: media.metadata!.messageSendAt,
);
final plaintextContent = Uint8List.fromList(
gzip.encode(utf8.encode(jsonEncode(msg.toJson()))),
);
final contact = await twonlyDB.contactsDao
.getContactByUserId(message.contactId)
.getSingleOrNull();
if (contact == null || contact.deleted) {
Log.warn(
'Contact deleted ${message.contactId} or not found in database.',
await twonlyDB.contactsDao.incFlameCounter(
groupMember.contactId,
false,
message.createdAt,
);
await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId,
const MessagesCompanion(errorWhileSending: Value(true)),
final downloadToken = getRandomUint8List(32);
var type = EncryptedContent_Media_Type.IMAGE;
if (media.mediaFile.type == MediaType.video) {
type = EncryptedContent_Media_Type.VIDEO;
} else if (media.mediaFile.type == MediaType.gif) {
type = EncryptedContent_Media_Type.GIF;
}
final notEncryptedContent = EncryptedContent(
media: EncryptedContent_Media(
senderMessageId: message.messageId,
type: type,
requiresAuthentication: media.mediaFile.requiresAuthentication,
timestamp: Int64(message.createdAt.millisecondsSinceEpoch),
downloadToken: media.mediaFile.downloadToken,
encryptionKey: media.mediaFile.encryptionKey,
encryptionNonce: media.mediaFile.encryptionNonce,
encryptionMac: media.mediaFile.encryptionMac,
),
);
continue;
if (media.mediaFile.displayLimitInMilliseconds != null) {
notEncryptedContent.media.displayLimitInMilliseconds =
Int64(media.mediaFile.displayLimitInMilliseconds!);
}
final cipherText = await sendCipherText(
groupMember.contactId,
notEncryptedContent,
onlyReturnEncryptedData: true,
);
if (cipherText == null) {
Log.error(
'Could not generate ciphertext message for ${groupMember.contactId}');
}
final messageOnSuccess = TextMessage()
..body = cipherText!.$1
..userId = Int64(groupMember.contactId);
if (cipherText.$2 != null) {
messageOnSuccess.pushData = cipherText.$2!;
}
messagesOnSuccess.add(messageOnSuccess);
downloadTokens.add(downloadToken);
}
await twonlyDB.contactsDao.incFlameCounter(
message.contactId,
false,
message.sendAt,
);
final encryptedBytes = await signalEncryptMessage(
message.contactId,
plaintextContent,
);
if (encryptedBytes == null) continue;
final messageOnSuccess = TextMessage()
..body = encryptedBytes
..userId = Int64(message.contactId);
final pushKind = (media.metadata!.isRealTwonly)
? PushKind.twonly
: (media.metadata!.isVideo)
? PushKind.video
: PushKind.image;
final pushData = await getPushData(
message.contactId,
PushNotification(
messageId: Int64(message.messageId),
kind: pushKind,
),
);
if (pushData != null) {
messageOnSuccess.pushData = pushData.toList();
}
messagesOnSuccess.add(messageOnSuccess);
downloadTokens.add(downloadToken);
}
final bytesToUpload = await media.encryptedPath.readAsBytes();
final uploadRequest = UploadRequest(
messagesOnSuccess: messagesOnSuccess,
downloadTokens: downloadTokens,
@ -469,6 +195,10 @@ Future<void> handleMediaUpload(MediaUpload media) async {
final uploadRequestBytes = uploadRequest.writeToBuffer();
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
}
Future<void> _uploadUploadRequest(MediaFileService media) async {
final apiAuthTokenRaw = await const FlutterSecureStorage()
.read(key: SecureStorageKeys.apiAuthToken);
if (apiAuthTokenRaw == null) {
@ -477,108 +207,27 @@ Future<void> handleMediaUpload(MediaUpload media) async {
}
final apiAuthToken = uint8ListToHex(base64Decode(apiAuthTokenRaw));
final uploadRequestFile = await writeSendMediaFile(
media.mediaUploadId,
'upload',
uploadRequestBytes,
);
final apiUrl =
'http${apiService.apiSecure}://${apiService.apiHost}/api/upload';
try {
Log.info('Starting upload from ${media.mediaUploadId}');
// try {
Log.info('Starting upload from ${media.mediaFile.mediaId}');
final task = UploadTask.fromFile(
taskId: 'upload_${media.mediaUploadId}',
displayName: (media.metadata?.isVideo ?? false) ? 'image' : 'video',
file: uploadRequestFile,
url: apiUrl,
priority: 0,
retries: 10,
headers: {
'x-twonly-auth-token': apiAuthToken,
},
);
currentUploadTasks[media.mediaUploadId] = task;
try {
await uploadFileFast(media, uploadRequestBytes, apiUrl, apiAuthToken);
} catch (e) {
Log.error('Fast upload failed: $e. Using slow method directly.');
await enqueueUploadTask(media.mediaUploadId);
}
} catch (e) {
Log.error('Exception during upload: $e');
}
}
Map<int, UploadTask> currentUploadTasks = {};
Future<void> enqueueUploadTask(int mediaUploadId) async {
if (currentUploadTasks[mediaUploadId] == null) {
Log.info('could not enqueue upload task: $mediaUploadId');
return;
}
Log.info('Enqueue upload task: $mediaUploadId');
await FileDownloader().enqueue(currentUploadTasks[mediaUploadId]!);
currentUploadTasks.remove(mediaUploadId);
await twonlyDB.mediaUploadsDao.updateMediaUpload(
mediaUploadId,
const MediaUploadsCompanion(
state: Value(UploadState.uploadTaskStarted),
),
);
}
Future<void> handleUploadWhenAppGoesBackground() async {
if (currentUploadTasks.keys.isEmpty) {
return;
}
Log.info('App goes into background. Enqueue uploads to the background.');
final keys = currentUploadTasks.keys.toList();
for (final key in keys) {
await enqueueUploadTask(key);
}
}
Future<void> uploadFileFast(
MediaUpload media,
Uint8List uploadRequestFile,
String apiUrl,
String apiAuthToken,
) async {
final requestMultipart = http.MultipartRequest(
'POST',
Uri.parse(apiUrl),
);
requestMultipart.headers['x-twonly-auth-token'] = apiAuthToken;
requestMultipart.files.add(
http.MultipartFile.fromBytes(
'file',
uploadRequestFile,
filename: 'upload',
),
final task = UploadTask.fromFile(
taskId: 'upload_${media.mediaFile.mediaId}',
displayName: media.mediaFile.type.name,
file: media.uploadRequestPath,
url: apiUrl,
priority: 0,
retries: 10,
headers: {
'x-twonly-auth-token': apiAuthToken,
},
);
final response = await requestMultipart.send();
if (response.statusCode == 200) {
Log.info('Upload successful!');
await handleUploadSuccess(media);
return;
} else if (response.statusCode == 429) {
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploadLimitReached),
),
);
} else {
Log.info('Upload failed with status: ${response.statusCode}');
}
Log.info('Enqueue upload task: ${task.taskId}');
await FileDownloader().enqueue(task);
await media.setUploadState(UploadState.backgroundUploadTaskStarted);
}

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

View file

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

View file

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

View file

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

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

View file

@ -1,30 +1,22 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path/path.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
class SaveToGalleryButton extends StatefulWidget {
const SaveToGalleryButton({
required this.getMergedImage,
required this.isLoading,
required this.displayButtonLabel,
required this.mediaService,
super.key,
this.mediaUploadId,
this.videoFilePath,
});
final Future<Uint8List?> Function() getMergedImage;
final bool displayButtonLabel;
final File? videoFilePath;
final int? mediaUploadId;
final MediaFileService mediaService;
final bool isLoading;
@override
@ -54,44 +46,20 @@ class SaveToGalleryButtonState extends State<SaveToGalleryButton> {
});
String? res;
var memoryPath = await getMediaBaseFilePath('memories');
if (widget.mediaUploadId != null) {
memoryPath = join(memoryPath, '${widget.mediaUploadId!}');
} else {
final random = Random();
final token = uint8ListToHex(
List<int>.generate(32, (i) => random.nextInt(256)),
);
memoryPath = join(memoryPath, token);
}
final user = await getUser();
if (user == null) return;
final storeToGallery = user.storeMediaFilesInGallery;
final storedMediaPath = widget.mediaService.storedPath;
if (widget.videoFilePath != null) {
memoryPath += '.mp4';
await File(widget.videoFilePath!.path).copy(memoryPath);
unawaited(createThumbnailsForVideo(File(memoryPath)));
if (storeToGallery) {
res = await saveVideoToGallery(widget.videoFilePath!.path);
}
} else {
final imageBytes = await widget.getMergedImage();
if (imageBytes == null || !mounted) return;
final webPImageBytes =
await FlutterImageCompress.compressWithList(
format: CompressFormat.webp,
imageBytes,
quality: 100,
);
memoryPath += '.png';
await File(memoryPath).writeAsBytes(webPImageBytes);
unawaited(createThumbnailsForImage(File(memoryPath)));
if (storeToGallery) {
res = await saveImageToGallery(imageBytes);
}
final storeToGallery = gUser.storeMediaFilesInGallery;
await widget.mediaService.storeMediaFile();
if (storeToGallery) {
res = await saveVideoToGallery(storedMediaPath.path);
}
await widget.mediaService.compressMedia();
await widget.mediaService.createThumbnail();
if (res == null) {
setState(() {
_imageSaved = true;

View file

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

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

View file

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

View file

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