diff --git a/lib/app.dart b/lib/app.dart index 36c9c2d..96588e9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,7 +6,7 @@ import 'package:twonly/globals.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/home.view.dart'; diff --git a/lib/globals.dart b/lib/globals.dart index 809be94..d08f1fa 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -1,5 +1,6 @@ import 'package:camera/camera.dart'; import 'package:twonly/src/database/twonly.db.dart'; +import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/services/api.service.dart'; late ApiService apiService; @@ -9,6 +10,10 @@ late TwonlyDB twonlyDB; List gCameras = []; +// Cached UserData in the memory. Every time the user data is changed the `updateUserdata` function is called, +// which will update this global variable. The variable is set in the main.dart and after the user has registered in the register.view.dart +late UserData gUser; + // The following global function can be called from anywhere to update // the UI when something changed. The callbacks will be set by // App widget. diff --git a/lib/main.dart b/lib/main.dart index 8751c01..431626c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,8 +10,8 @@ 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/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.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/fcm.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; @@ -28,6 +28,7 @@ void main() async { final user = await getUser(); if (user != null) { + gUser = user; if (user.isDemoUser) { await deleteLocalUserData(); } diff --git a/lib/src/database/daos/messages.dao.dart b/lib/src/database/daos/messages.dao.dart index ed29987..07fd0a6 100644 --- a/lib/src/database/daos/messages.dao.dart +++ b/lib/src/database/daos/messages.dao.dart @@ -5,7 +5,7 @@ import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; -import 'package:twonly/src/services/mediafile.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; part 'messages.dao.g.dart'; diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 8b44793..8818f0a 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -10,10 +10,21 @@ enum MediaType { } enum UploadState { - pending, - readyToUpload, - uploadTaskStarted, - receiverNotified, + // Image/Video was taken. A database entry was created to track it... + initialized, + // 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, + uploading, + backgroundUploadTaskStarted, + uploaded, + + uploadLimitReached, + // readyToUpload, + // uploadTaskStarted, + // receiverNotified, } enum DownloadState { @@ -33,7 +44,8 @@ class MediaFiles extends Table { TextColumn get uploadState => textEnum().nullable()(); TextColumn get downloadState => textEnum().nullable()(); - BoolColumn get requiresAuthentication => boolean()(); + BoolColumn get requiresAuthentication => + boolean().withDefault(const Constant(false))(); BoolColumn get reopenByContact => boolean().withDefault(const Constant(false))(); diff --git a/lib/src/database/tables/messages.table.dart b/lib/src/database/tables/messages.table.dart index b7ef767..08c28f0 100644 --- a/lib/src/database/tables/messages.table.dart +++ b/lib/src/database/tables/messages.table.dart @@ -18,6 +18,8 @@ class Messages extends Table { TextColumn get mediaId => text().nullable().references(MediaFiles, #mediaId)(); + BlobColumn get downloadToken => blob().nullable()(); + TextColumn get quotesMessageId => text().nullable().references(Messages, #messageId)(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index d69d580..f3292b9 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1460,9 +1460,10 @@ class $MediaFilesTable extends MediaFiles late final GeneratedColumn requiresAuthentication = GeneratedColumn('requires_authentication', aliasedName, false, type: DriftSqlType.bool, - requiredDuringInsert: true, + requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("requires_authentication" IN (0, 1))')); + 'CHECK ("requires_authentication" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _reopenByContactMeta = const VerificationMeta('reopenByContact'); @override @@ -1563,8 +1564,6 @@ class $MediaFilesTable extends MediaFiles _requiresAuthenticationMeta, requiresAuthentication.isAcceptableOrUnknown( data['requires_authentication']!, _requiresAuthenticationMeta)); - } else if (isInserting) { - context.missing(_requiresAuthenticationMeta); } if (data.containsKey('reopen_by_contact')) { context.handle( @@ -2017,7 +2016,7 @@ class MediaFilesCompanion extends UpdateCompanion { required MediaType type, this.uploadState = const Value.absent(), this.downloadState = const Value.absent(), - required bool requiresAuthentication, + this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.stored = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), @@ -2028,8 +2027,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.encryptionNonce = const Value.absent(), this.createdAt = const Value.absent(), this.rowid = const Value.absent(), - }) : type = Value(type), - requiresAuthentication = Value(requiresAuthentication); + }) : type = Value(type); static Insertable custom({ Expression? mediaId, Expression? type, @@ -2233,6 +2231,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( 'REFERENCES media_files (media_id)')); + static const VerificationMeta _downloadTokenMeta = + const VerificationMeta('downloadToken'); + @override + late final GeneratedColumn downloadToken = + GeneratedColumn('download_token', aliasedName, true, + type: DriftSqlType.blob, requiredDuringInsert: false); static const VerificationMeta _quotesMessageIdMeta = const VerificationMeta('quotesMessageId'); @override @@ -2319,6 +2323,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { senderId, content, mediaId, + downloadToken, quotesMessageId, isDeletedFromSender, isEdited, @@ -2361,6 +2366,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> { context.handle(_mediaIdMeta, mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); } + if (data.containsKey('download_token')) { + context.handle( + _downloadTokenMeta, + downloadToken.isAcceptableOrUnknown( + data['download_token']!, _downloadTokenMeta)); + } if (data.containsKey('quotes_message_id')) { context.handle( _quotesMessageIdMeta, @@ -2428,6 +2439,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']), + downloadToken: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}download_token']), quotesMessageId: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), isDeletedFromSender: attachedDatabase.typeMapping.read( @@ -2461,6 +2474,7 @@ class Message extends DataClass implements Insertable { final int? senderId; final String? content; final String? mediaId; + final Uint8List? downloadToken; final String? quotesMessageId; final bool isDeletedFromSender; final bool isEdited; @@ -2476,6 +2490,7 @@ class Message extends DataClass implements Insertable { this.senderId, this.content, this.mediaId, + this.downloadToken, this.quotesMessageId, required this.isDeletedFromSender, required this.isEdited, @@ -2499,6 +2514,9 @@ class Message extends DataClass implements Insertable { if (!nullToAbsent || mediaId != null) { map['media_id'] = Variable(mediaId); } + if (!nullToAbsent || downloadToken != null) { + map['download_token'] = Variable(downloadToken); + } if (!nullToAbsent || quotesMessageId != null) { map['quotes_message_id'] = Variable(quotesMessageId); } @@ -2530,6 +2548,9 @@ class Message extends DataClass implements Insertable { mediaId: mediaId == null && nullToAbsent ? const Value.absent() : Value(mediaId), + downloadToken: downloadToken == null && nullToAbsent + ? const Value.absent() + : Value(downloadToken), quotesMessageId: quotesMessageId == null && nullToAbsent ? const Value.absent() : Value(quotesMessageId), @@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable { senderId: serializer.fromJson(json['senderId']), content: serializer.fromJson(json['content']), mediaId: serializer.fromJson(json['mediaId']), + downloadToken: serializer.fromJson(json['downloadToken']), quotesMessageId: serializer.fromJson(json['quotesMessageId']), isDeletedFromSender: serializer.fromJson(json['isDeletedFromSender']), @@ -2578,6 +2600,7 @@ class Message extends DataClass implements Insertable { 'senderId': serializer.toJson(senderId), 'content': serializer.toJson(content), 'mediaId': serializer.toJson(mediaId), + 'downloadToken': serializer.toJson(downloadToken), 'quotesMessageId': serializer.toJson(quotesMessageId), 'isDeletedFromSender': serializer.toJson(isDeletedFromSender), 'isEdited': serializer.toJson(isEdited), @@ -2596,6 +2619,7 @@ class Message extends DataClass implements Insertable { Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), bool? isDeletedFromSender, bool? isEdited, @@ -2611,6 +2635,8 @@ class Message extends DataClass implements Insertable { senderId: senderId.present ? senderId.value : this.senderId, content: content.present ? content.value : this.content, mediaId: mediaId.present ? mediaId.value : this.mediaId, + downloadToken: + downloadToken.present ? downloadToken.value : this.downloadToken, quotesMessageId: quotesMessageId.present ? quotesMessageId.value : this.quotesMessageId, @@ -2630,6 +2656,9 @@ class Message extends DataClass implements Insertable { senderId: data.senderId.present ? data.senderId.value : this.senderId, content: data.content.present ? data.content.value : this.content, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, + downloadToken: data.downloadToken.present + ? data.downloadToken.value + : this.downloadToken, quotesMessageId: data.quotesMessageId.present ? data.quotesMessageId.value : this.quotesMessageId, @@ -2658,6 +2687,7 @@ class Message extends DataClass implements Insertable { ..write('senderId: $senderId, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') @@ -2678,6 +2708,7 @@ class Message extends DataClass implements Insertable { senderId, content, mediaId, + $driftBlobEquality.hash(downloadToken), quotesMessageId, isDeletedFromSender, isEdited, @@ -2696,6 +2727,7 @@ class Message extends DataClass implements Insertable { other.senderId == this.senderId && other.content == this.content && other.mediaId == this.mediaId && + $driftBlobEquality.equals(other.downloadToken, this.downloadToken) && other.quotesMessageId == this.quotesMessageId && other.isDeletedFromSender == this.isDeletedFromSender && other.isEdited == this.isEdited && @@ -2713,6 +2745,7 @@ class MessagesCompanion extends UpdateCompanion { final Value senderId; final Value content; final Value mediaId; + final Value downloadToken; final Value quotesMessageId; final Value isDeletedFromSender; final Value isEdited; @@ -2729,6 +2762,7 @@ class MessagesCompanion extends UpdateCompanion { this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), @@ -2746,6 +2780,7 @@ class MessagesCompanion extends UpdateCompanion { this.senderId = const Value.absent(), this.content = const Value.absent(), this.mediaId = const Value.absent(), + this.downloadToken = const Value.absent(), this.quotesMessageId = const Value.absent(), this.isDeletedFromSender = const Value.absent(), this.isEdited = const Value.absent(), @@ -2763,6 +2798,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? senderId, Expression? content, Expression? mediaId, + Expression? downloadToken, Expression? quotesMessageId, Expression? isDeletedFromSender, Expression? isEdited, @@ -2780,6 +2816,7 @@ class MessagesCompanion extends UpdateCompanion { if (senderId != null) 'sender_id': senderId, if (content != null) 'content': content, if (mediaId != null) 'media_id': mediaId, + if (downloadToken != null) 'download_token': downloadToken, if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, if (isDeletedFromSender != null) 'is_deleted_from_sender': isDeletedFromSender, @@ -2800,6 +2837,7 @@ class MessagesCompanion extends UpdateCompanion { Value? senderId, Value? content, Value? mediaId, + Value? downloadToken, Value? quotesMessageId, Value? isDeletedFromSender, Value? isEdited, @@ -2816,6 +2854,7 @@ class MessagesCompanion extends UpdateCompanion { senderId: senderId ?? this.senderId, content: content ?? this.content, mediaId: mediaId ?? this.mediaId, + downloadToken: downloadToken ?? this.downloadToken, quotesMessageId: quotesMessageId ?? this.quotesMessageId, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isEdited: isEdited ?? this.isEdited, @@ -2847,6 +2886,9 @@ class MessagesCompanion extends UpdateCompanion { if (mediaId.present) { map['media_id'] = Variable(mediaId.value); } + if (downloadToken.present) { + map['download_token'] = Variable(downloadToken.value); + } if (quotesMessageId.present) { map['quotes_message_id'] = Variable(quotesMessageId.value); } @@ -2888,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('senderId: $senderId, ') ..write('content: $content, ') ..write('mediaId: $mediaId, ') + ..write('downloadToken: $downloadToken, ') ..write('quotesMessageId: $quotesMessageId, ') ..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isEdited: $isEdited, ') @@ -7100,7 +7143,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ required MediaType type, Value uploadState, Value downloadState, - required bool requiresAuthentication, + Value requiresAuthentication, Value reopenByContact, Value stored, Value?> reuploadRequestedBy, @@ -7433,7 +7476,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< required MediaType type, Value uploadState = const Value.absent(), Value downloadState = const Value.absent(), - required bool requiresAuthentication, + Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value stored = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), @@ -7513,6 +7556,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, Value isEdited, @@ -7530,6 +7574,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value senderId, Value content, Value mediaId, + Value downloadToken, Value quotesMessageId, Value isDeletedFromSender, Value isEdited, @@ -7671,6 +7716,9 @@ class $$MessagesTableFilterComposer ColumnFilters get content => $composableBuilder( column: $table.content, builder: (column) => ColumnFilters(column)); + ColumnFilters get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => ColumnFilters(column)); + ColumnFilters get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => ColumnFilters(column)); @@ -7856,6 +7904,10 @@ class $$MessagesTableOrderingComposer ColumnOrderings get content => $composableBuilder( column: $table.content, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get downloadToken => $composableBuilder( + column: $table.downloadToken, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => ColumnOrderings(column)); @@ -7978,6 +8030,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get content => $composableBuilder(column: $table.content, builder: (column) => column); + GeneratedColumn get downloadToken => $composableBuilder( + column: $table.downloadToken, builder: (column) => column); + GeneratedColumn get isDeletedFromSender => $composableBuilder( column: $table.isDeletedFromSender, builder: (column) => column); @@ -8181,6 +8236,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), @@ -8198,6 +8254,7 @@ class $$MessagesTableTableManager extends RootTableManager< senderId: senderId, content: content, mediaId: mediaId, + downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, @@ -8215,6 +8272,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value senderId = const Value.absent(), Value content = const Value.absent(), Value mediaId = const Value.absent(), + Value downloadToken = const Value.absent(), Value quotesMessageId = const Value.absent(), Value isDeletedFromSender = const Value.absent(), Value isEdited = const Value.absent(), @@ -8232,6 +8290,7 @@ class $$MessagesTableTableManager extends RootTableManager< senderId: senderId, content: content, mediaId: mediaId, + downloadToken: downloadToken, quotesMessageId: quotesMessageId, isDeletedFromSender: isDeletedFromSender, isEdited: isEdited, diff --git a/lib/src/model/memory_item.model.dart b/lib/src/model/memory_item.model.dart index eafe7a8..de2ce86 100644 --- a/lib/src/model/memory_item.model.dart +++ b/lib/src/model/memory_item.model.dart @@ -5,8 +5,8 @@ import 'package:drift/drift.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/message_old.dart'; -import 'package:twonly/src/services/api/media_upload.dart' as send; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; class MemoryItem { MemoryItem({ diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 07b2dcd..64bf2b5 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -23,8 +23,8 @@ import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; 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/media_download.dart'; -import 'package:twonly/src/services/api/media_upload.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'; diff --git a/lib/src/services/api/media_download.dart b/lib/src/services/api/mediafiles/download.service.dart similarity index 98% rename from lib/src/services/api/media_download.dart rename to lib/src/services/api/mediafiles/download.service.dart index 67bc7f5..4b1b738 100644 --- a/lib/src/services/api/media_download.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -12,10 +12,10 @@ 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/model/protobuf/client/generated/messages.pbserver.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; import 'package:twonly/src/services/api/messages.dart'; -import 'package:twonly/src/services/mediafile.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; Future tryDownloadAllMediaFiles({bool force = false}) async { diff --git a/lib/src/services/api/mediafiles/media_background.service.dart b/lib/src/services/api/mediafiles/media_background.service.dart new file mode 100644 index 0000000..dce9a1c --- /dev/null +++ b/lib/src/services/api/mediafiles/media_background.service.dart @@ -0,0 +1,50 @@ +import 'dart:async'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/foundation.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/twonly_safe/create_backup.twonly_safe.dart'; +import 'package:twonly/src/utils/log.dart'; + +Future initFileDownloader() async { + FileDownloader().updates.listen((update) async { + switch (update) { + case TaskStatusUpdate(): + if (update.task.taskId.contains('upload_')) { + await handleUploadStatusUpdate(update); + } + if (update.task.taskId.contains('download_')) { + await handleDownloadStatusUpdate(update); + } + if (update.task.taskId.contains('backup')) { + await handleBackupStatusUpdate(update); + } + case TaskProgressUpdate(): + Log.info( + 'Progress update for ${update.task} with progress ${update.progress}', + ); + } + }); + + await FileDownloader().start(); + + try { + var androidConfig = []; + if (kDebugMode) { + androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)]; + } + await FileDownloader().configure(androidConfig: androidConfig); + } catch (e) { + Log.error(e); + } + + if (kDebugMode) { + FileDownloader().configureNotification( + running: const TaskNotification( + 'Uploading/Downloading', + '{filename} ({progress}).', + ), + progressBar: true, + ); + } +} diff --git a/lib/src/services/api/media_upload.dart b/lib/src/services/api/mediafiles/upload.service.dart similarity index 55% rename from lib/src/services/api/media_upload.dart rename to lib/src/services/api/mediafiles/upload.service.dart index 364eb1f..d0f4350 100644 --- a/lib/src/services/api/media_upload.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -2,7 +2,6 @@ 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'; @@ -17,14 +16,13 @@ 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/media_uploads_table.dart'; -import 'package:twonly/src/database/tables/messages_table.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/model/protobuf/push_notification/push_notification.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.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'; @@ -33,76 +31,6 @@ import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:video_compress/video_compress.dart'; -Future isAllowedToSend() async { - final user = await getUser(); - if (user == null) return null; - if (user.subscriptionPlan == 'Free') { - var todaysImageCounter = user.todaysImageCounter; - if (user.lastImageSend != null && user.todaysImageCounter != null) { - if (isToday(user.lastImageSend!)) { - if (user.todaysImageCounter == 10) { - return ErrorCode.PlanLimitReached; - } - todaysImageCounter = user.todaysImageCounter! + 1; - } else { - todaysImageCounter = 1; - } - } else { - todaysImageCounter = 1; - } - await updateUserdata((user) { - user - ..lastImageSend = DateTime.now() - ..todaysImageCounter = todaysImageCounter; - return user; - }); - } - return null; -} - -Future initFileDownloader() async { - FileDownloader().updates.listen((update) async { - switch (update) { - case TaskStatusUpdate(): - if (update.task.taskId.contains('upload_')) { - await handleUploadStatusUpdate(update); - } - if (update.task.taskId.contains('download_')) { - await handleDownloadStatusUpdate(update); - } - if (update.task.taskId.contains('backup')) { - await handleBackupStatusUpdate(update); - } - case TaskProgressUpdate(): - Log.info( - 'Progress update for ${update.task} with progress ${update.progress}', - ); - } - }); - - await FileDownloader().start(); - - try { - var androidConfig = []; - if (kDebugMode) { - androidConfig = [(Config.bypassTLSCertificateValidation, kDebugMode)]; - } - await FileDownloader().configure(androidConfig: androidConfig); - } catch (e) { - Log.error(e); - } - - if (kDebugMode) { - FileDownloader().configureNotification( - running: const TaskNotification( - 'Uploading/Downloading', - '{filename} ({progress}).', - ), - progressBar: true, - ); - } -} - /// States: /// when user recorded an video /// 1. Compress video @@ -115,43 +43,43 @@ Future initFileDownloader() async { /// Create a new entry in the database -Future checkForFailedUploads() async { - final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); - final mediaUploadIds = []; - for (final message in messages) { - if (mediaUploadIds.contains(message.mediaUploadId)) { - continue; - } - final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( - message.mediaUploadId!, - const MediaUploadsCompanion( - state: Value(UploadState.pending), - encryptionData: Value( - null, // start from scratch e.q. encrypt the files again if already happen - ), - ), - ); - if (affectedRows == 0) { - Log.error( - 'The media from message ${message.messageId} already deleted.', - ); - await twonlyDB.messagesDao.updateMessageByMessageId( - message.messageId, - const MessagesCompanion( - errorWhileSending: Value(true), - ), - ); - } else { - mediaUploadIds.add(message.mediaUploadId!); - } - } - if (messages.isNotEmpty) { - Log.error( - 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', - ); - } - return mediaUploadIds.isNotEmpty; // return true if there are affected -} +// Future checkForFailedUploads() async { +// final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); +// final mediaUploadIds = []; +// for (final message in messages) { +// if (mediaUploadIds.contains(message.mediaUploadId)) { +// continue; +// } +// final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( +// message.mediaUploadId!, +// const MediaUploadsCompanion( +// state: Value(UploadState.pending), +// encryptionData: Value( +// null, // start from scratch e.q. encrypt the files again if already happen +// ), +// ), +// ); +// if (affectedRows == 0) { +// Log.error( +// 'The media from message ${message.messageId} already deleted.', +// ); +// await twonlyDB.messagesDao.updateMessageByMessageId( +// message.messageId, +// const MessagesCompanion( +// errorWhileSending: Value(true), +// ), +// ); +// } else { +// mediaUploadIds.add(message.mediaUploadId!); +// } +// } +// if (messages.isNotEmpty) { +// Log.error( +// 'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', +// ); +// } +// return mediaUploadIds.isNotEmpty; // return true if there are affected +// } final lockingHandleMediaFile = Mutex(); Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { @@ -192,82 +120,25 @@ Future retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { } } -Future initMediaUpload() async { - return twonlyDB.mediaUploadsDao - .insertMediaUpload(const MediaUploadsCompanion()); -} - -Future addVideoToUpload(int mediaUploadId, File videoFilePath) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - await videoFilePath.copy('$basePath.original.mp4'); - return compressVideoIfExists(mediaUploadId); -} - -Future addOrModifyImageToUpload( - int mediaUploadId, - Uint8List imageBytes, +Future initializeMediaUpload( + MediaType type, + int? displayLimitInMilliseconds, ) async { - Uint8List imageBytesCompressed; + final chacha20 = FlutterChacha20.poly1305Aead(); + final encryptionKey = await (await chacha20.newSecretKey()).extract(); + final encryptionNonce = chacha20.newNonce(); - final stopwatch = Stopwatch()..start(); - - Log.info('Raw images size in bytes: ${imageBytes.length}'); - - try { - imageBytesCompressed = await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - // minHeight: 0, - // minWidth: 0, - imageBytes, - quality: 90, - ); - - if (imageBytesCompressed.length >= 1 * 1000 * 1000) { - // if the media file is over 2MB compress it with 60% - imageBytesCompressed = await FlutterImageCompress.compressWithList( - format: CompressFormat.webp, - imageBytes, - quality: 60, - ); - } - await writeSendMediaFile(mediaUploadId, 'png', imageBytesCompressed); - } catch (e) { - Log.error('$e'); - // as a fall back use the original image - await writeSendMediaFile(mediaUploadId, 'png', imageBytes); - imageBytesCompressed = imageBytes; - } - - stopwatch.stop(); - - Log.info( - 'Compression the image took: ${stopwatch.elapsedMilliseconds} milliseconds', - ); - Log.info('Raw images size in bytes: ${imageBytesCompressed.length}'); - - // stopwatch.reset(); - // stopwatch.start(); - - // // var helper = MediaUploadHelper(); - // try { - // final webpBytes = - // await convertAndCompressImage(pngRawImageBytes: imageBytes); - // Log.info( - // 'Compression the image in rust took: ${stopwatch.elapsedMilliseconds} milliseconds'); - // Log.info("Raw images size in bytes using webp: ${webpBytes.length}"); - // } catch (e) { - // Log.error("$e"); - // } - - /// in case the media file was already encrypted of even uploaded - /// remove the data so it will be done again. - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - const MediaUploadsCompanion( - encryptionData: Value(null), + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( + MediaFilesCompanion( + uploadState: const Value(UploadState.initialized), + displayLimitInMilliseconds: Value(displayLimitInMilliseconds), + encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)), + encryptionNonce: Value(Uint8List.fromList(encryptionNonce)), + type: Value(type), ), ); - return imageBytesCompressed; + if (mediaFile == null) return null; + return MediaFileService.fromMedia(mediaFile); } Future handlePreProcessingState(MediaUpload media) async { @@ -304,7 +175,6 @@ Future encryptMediaFiles( final state = MediaEncryptionData(); final chacha20 = FlutterChacha20.poly1305Aead(); - final secretKey = await (await chacha20.newSecretKey()).extract(); state ..encryptionKey = secretKey.bytes @@ -338,70 +208,37 @@ Future encryptMediaFiles( } Future finalizeUpload( - int mediaUploadId, - List contactIds, - bool isRealTwonly, - bool isVideo, - bool mirrorVideo, - int maxShowTime, + MediaFileService mediaService, + List groupIds, ) async { - final metadata = MediaUploadMetadata() - ..contactIds = contactIds - ..isRealTwonly = isRealTwonly - ..messageSendAt = DateTime.now() - ..isVideo = isVideo - ..maxShowTime = maxShowTime - ..mirrorVideo = mirrorVideo; + final messageIds = []; - final messageIds = []; - - for (final contactId in contactIds) { - final messageId = await twonlyDB.messagesDao.insertMessage( + for (final groupId in groupIds) { + final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( - contactId: Value(contactId), - kind: const Value(MessageKind.media), - sendAt: Value(metadata.messageSendAt), - downloadState: const Value(DownloadState.pending), - mediaUploadId: Value(mediaUploadId), - contentJson: Value( - jsonEncode( - MediaMessageContent( - maxShowTime: maxShowTime, - isRealTwonly: isRealTwonly, - isVideo: isVideo, - mirrorVideo: mirrorVideo, - ).toJson(), - ), + groupId: Value(groupId), + mediaId: Value(mediaService.mediaFile.mediaId), + ), + ); + if (message != null) { + messageIds.add(message); + // de-archive contact when sending a new message + await twonlyDB.groupsDao.updateGroup( + message.groupId, + const GroupsCompanion( + archived: Value(false), ), - ), - ); - // de-archive contact when sending a new message - await twonlyDB.contactsDao.updateContact( - contactId, - const ContactsCompanion( - archived: Value(false), - ), - ); - if (messageId != null) { - messageIds.add(messageId); + ); } else { Log.error('Error inserting media upload message in database.'); } } - await twonlyDB.mediaUploadsDao.updateMediaUpload( - mediaUploadId, - MediaUploadsCompanion( - messageIds: Value(messageIds), - metadata: Value(metadata), - ), - ); - - unawaited(handleNextMediaUploadSteps(mediaUploadId)); + unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId)); } final lockingHandleNextMediaUploadStep = Mutex(); -Future handleNextMediaUploadSteps(int mediaUploadId) async { +Future handleNextMediaUploadSteps(String mediaUploadId) async { await lockingHandleNextMediaUploadStep.protect(() async { final mediaUpload = await twonlyDB.mediaUploadsDao .getMediaUploadById(mediaUploadId) @@ -549,7 +386,7 @@ Future handleMediaUpload(MediaUpload media) async { continue; } - final downloadToken = createDownloadToken(); + final downloadToken = getRandomUint8List(32); final msg = MessageJson( kind: MessageKind.media, @@ -734,159 +571,14 @@ Future uploadFileFast( 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}'); } } - -Future compressVideoIfExists(int mediaUploadId) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final videoOriginalFile = File('$basePath.original.mp4'); - final videoCompressedFile = File('$basePath.mp4'); - - if (videoCompressedFile.existsSync()) { - // file is already compressed and exists - return true; - } - - if (!videoOriginalFile.existsSync()) { - // media upload does not have a video - return false; - } - - final stopwatch = Stopwatch()..start(); - - MediaInfo? mediaInfo; - try { - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res1280x720Quality, - includeAudio: - true, // https://github.com/jonataslaw/VideoCompress/issues/184 - ); - - Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); - - if (mediaInfo.filesize! >= 30 * 1000 * 1000) { - // if the media file is over 20MB compress it with low quality - mediaInfo = await VideoCompress.compressVideo( - videoOriginalFile.path, - quality: VideoQuality.Res960x540Quality, - includeAudio: true, - ); - } - } catch (e) { - Log.error('during video compression: $e'); - } - stopwatch.stop(); - Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); - - if (mediaInfo == null) { - Log.error('could not compress video.'); - // as a fall back use the non compressed version - await videoOriginalFile.copy(videoCompressedFile.path); - await videoOriginalFile.delete(); - } else { - await mediaInfo.file!.copy(videoCompressedFile.path); - await mediaInfo.file!.delete(); - } - return true; -} - -/// --- helper functions --- - -Future readSendMediaFile(int mediaUploadId, String type) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - if (!file.existsSync()) { - throw Exception('$file not found'); - } - return file.readAsBytes(); -} - -Future writeSendMediaFile( - int mediaUploadId, - String type, - Uint8List data, -) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - await file.writeAsBytes(data); - return file; -} - -Future deleteSendMediaFile(int mediaUploadId, String type) async { - final basePath = await getMediaFilePath(mediaUploadId, 'send'); - final file = File('$basePath.$type'); - if (file.existsSync()) { - await file.delete(); - } -} - -Future getMediaFilePath(dynamic mediaId, String type) async { - final basedir = await getApplicationSupportDirectory(); - final mediaSendDir = Directory(join(basedir.path, 'media', type)); - if (!mediaSendDir.existsSync()) { - await mediaSendDir.create(recursive: true); - } - return join(mediaSendDir.path, '$mediaId'); -} - -Future getMediaBaseFilePath(String type) async { - final basedir = await getApplicationSupportDirectory(); - final mediaSendDir = Directory(join(basedir.path, 'media', type)); - if (!mediaSendDir.existsSync()) { - await mediaSendDir.create(recursive: true); - } - return mediaSendDir.path; -} - -/// combines two utf8 list -Uint8List combineUint8Lists(Uint8List list1, Uint8List list2) { - final combinedLength = 4 + list1.length + list2.length; - final combinedList = Uint8List(combinedLength); - ByteData.sublistView(combinedList).setInt32(0, list1.length); - combinedList - ..setRange(4, 4 + list1.length, list1) - ..setRange(4 + list1.length, combinedLength, list2); - return combinedList; -} - -List extractUint8Lists(Uint8List combinedList) { - final byteData = ByteData.sublistView(combinedList); - final sizeOfList1 = byteData.getInt32(0); - final list1 = Uint8List.view(combinedList.buffer, 4, sizeOfList1); - final list2 = Uint8List.view( - combinedList.buffer, - 4 + sizeOfList1, - combinedList.lengthInBytes - 4 - sizeOfList1, - ); - return [list1, list2]; -} - -Future purgeSendMediaFiles() async { - final basedir = await getApplicationSupportDirectory(); - final directory = Directory(join(basedir.path, 'media', 'send')); - await purgeMediaFiles(directory); -} - -String uint8ListToHex(List bytes) { - return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); -} - -Uint8List hexToUint8List(String hex) => Uint8List.fromList( - List.generate( - hex.length ~/ 2, - (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), - ), - ); - -Uint8List createDownloadToken() { - final random = Random(); - - final token = Uint8List(32); - for (var j = 0; j < 32; j++) { - token[j] = random.nextInt(256); // Generate a random byte (0-255) - } - return token; -} diff --git a/lib/src/services/api/server_messages/media.server_messages.dart b/lib/src/services/api/server_messages/media.server_messages.dart index bef08fa..ef7e433 100644 --- a/lib/src/services/api/server_messages/media.server_messages.dart +++ b/lib/src/services/api/server_messages/media.server_messages.dart @@ -4,9 +4,9 @@ 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/model/protobuf/client/generated/messages.pb.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/utils.dart'; -import 'package:twonly/src/services/mediafile.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; Future handleMedia( @@ -157,7 +157,7 @@ Future handleMediaUpdate( await twonlyDB.mediaFilesDao.updateMedia( mediaFile.mediaId, MediaFilesCompanion( - uploadState: const Value(UploadState.pending), + uploadState: const Value(UploadState.uploading), reuploadRequestedBy: Value(reuploadRequestedBy), ), ); diff --git a/lib/src/services/api/utils.dart b/lib/src/services/api/utils.dart index af05082..1bc0674 100644 --- a/lib/src/services/api/utils.dart +++ b/lib/src/services/api/utils.dart @@ -57,13 +57,7 @@ ClientToServer createClientToServerFromApplicationData( return ClientToServer()..v0 = v0; } -Future deleteContact(int contactId) async { - await twonlyDB.signalDao.deleteAllByContactId(contactId); - await deleteSessionWithTarget(contactId); - await twonlyDB.contactsDao.deleteContactByUserId(contactId); -} - -Future rejectUser(int contactId) async { +Future rejectAndDeleteContact(int contactId) async { await sendCipherText( contactId, EncryptedContent( @@ -72,6 +66,9 @@ Future rejectUser(int contactId) async { ), ), ); + await twonlyDB.signalDao.deleteAllByContactId(contactId); + await deleteSessionWithTarget(contactId); + await twonlyDB.contactsDao.deleteContactByUserId(contactId); } Future handleMediaError(MediaFile media) async { diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart new file mode 100644 index 0000000..8be5e6f --- /dev/null +++ b/lib/src/services/mediafiles/compression.service.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:video_compress/video_compress.dart'; + +Future compressImage( + File sourceFile, + File destinationFile, +) async { + final stopwatch = Stopwatch()..start(); + + try { + var compressedBytes = await FlutterImageCompress.compressWithFile( + sourceFile.path, + format: CompressFormat.webp, + quality: 90, + ); + + if (compressedBytes == null) { + throw Exception( + 'Could not compress media file: $sourceFile. Sending original file.', + ); + } + + Log.info('Compressed images size in bytes: ${compressedBytes.length}'); + + if (compressedBytes.length >= 1 * 1000 * 1000) { + // if the media file is over 1MB compress it with 60% + final tmpCompressedBytes = await FlutterImageCompress.compressWithFile( + sourceFile.path, + format: CompressFormat.webp, + quality: 60, + ); + if (tmpCompressedBytes != null) { + Log.error( + 'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.', + ); + compressedBytes = tmpCompressedBytes; + } + } + + await destinationFile.writeAsBytes(compressedBytes); + } catch (e) { + Log.error('$e'); + sourceFile.copySync(destinationFile.path); + } + + stopwatch.stop(); + + Log.info( + 'Compression of the image took: ${stopwatch.elapsedMilliseconds} milliseconds.', + ); +} + +Future compressVideo( + File sourceFile, + File destinationFile, +) async { + final stopwatch = Stopwatch()..start(); + + MediaInfo? mediaInfo; + try { + mediaInfo = await VideoCompress.compressVideo( + sourceFile.path, + quality: VideoQuality.Res1280x720Quality, + includeAudio: + true, // https://github.com/jonataslaw/VideoCompress/issues/184 + ); + + Log.info('Video has now size of ${mediaInfo!.filesize} bytes.'); + + if (mediaInfo.filesize! >= 30 * 1000 * 1000) { + // if the media file is over 20MB compress it with low quality + mediaInfo = await VideoCompress.compressVideo( + sourceFile.path, + quality: VideoQuality.Res960x540Quality, + includeAudio: true, + ); + } + } catch (e) { + Log.error('during video compression: $e'); + } + stopwatch.stop(); + Log.info('It took ${stopwatch.elapsedMilliseconds}ms to compress the video'); + + if (mediaInfo == null) { + Log.error('Could not compress video using original video.'); + // as a fall back use the non compressed version + sourceFile.copySync(destinationFile.path); + } else { + await mediaInfo.file!.copy(destinationFile.path); + } +} diff --git a/lib/src/services/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart similarity index 65% rename from lib/src/services/mediafile.service.dart rename to lib/src/services/mediafiles/mediafile.service.dart index cc2b3f1..13ce178 100644 --- a/lib/src/services/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -1,12 +1,12 @@ import 'dart:io'; - import 'package:drift/drift.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.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/thumbnail.service.dart'; +import 'package:twonly/src/services/mediafiles/compression.service.dart'; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/utils/log.dart'; class MediaFileService { @@ -39,6 +39,28 @@ class MediaFileService { } } + Future setDisplayLimit(int? displayLimitInMilliseconds) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + displayLimitInMilliseconds: Value(displayLimitInMilliseconds), + ), + ); + await updateFromDB(); + } + + Future setRequiresAuth(bool requiresAuthentication) async { + await twonlyDB.mediaFilesDao.updateMedia( + mediaFile.mediaId, + MediaFilesCompanion( + requiresAuthentication: Value(requiresAuthentication), + displayLimitInMilliseconds: + requiresAuthentication ? const Value(12) : const Value.absent(), + ), + ); + await updateFromDB(); + } + Future createThumbnail() async { if (!storedPath.existsSync()) { Log.error('Could not create Thumbnail as stored media does not exists.'); @@ -54,18 +76,35 @@ class MediaFileService { } } + Future compressMedia() async { + if (!originalPath.existsSync()) { + Log.error('Could not compress as original media does not exists.'); + return; + } + switch (mediaFile.type) { + case MediaType.image: + await compressImage(originalPath, tempPath); + case MediaType.video: + await compressVideo(originalPath, tempPath); + case MediaType.gif: + originalPath.renameSync(tempPath.path); + Log.error('Compression for .gif is not implemented yet.'); + } + } + void fullMediaRemoval() { - if (tempPath.existsSync()) { - tempPath.deleteSync(); - } - if (encryptedPath.existsSync()) { - encryptedPath.deleteSync(); - } - if (storedPath.existsSync()) { - storedPath.deleteSync(); - } - if (thumbnailPath.existsSync()) { - thumbnailPath.deleteSync(); + final pathsToRemove = [ + tempPath, + encryptedPath, + originalPath, + storedPath, + thumbnailPath + ]; + + for (final path in pathsToRemove) { + if (path.existsSync()) { + path.deleteSync(); + } } } @@ -121,4 +160,8 @@ class MediaFileService { 'tmp', namePrefix: '.encrypted', ); + File get originalPath => _buildFilePath( + 'tmp', + namePrefix: '.original', + ); } diff --git a/lib/src/services/thumbnail.service.dart b/lib/src/services/mediafiles/thumbnail.service.dart similarity index 100% rename from lib/src/services/thumbnail.service.dart rename to lib/src/services/mediafiles/thumbnail.service.dart diff --git a/lib/src/services/twonly_safe/common.twonly_safe.dart b/lib/src/services/twonly_safe/common.twonly_safe.dart index 27d2189..5a6426e 100644 --- a/lib/src/services/twonly_safe/common.twonly_safe.dart +++ b/lib/src/services/twonly_safe/common.twonly_safe.dart @@ -4,7 +4,7 @@ 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/media_upload.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/storage.dart'; diff --git a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart index b31cc35..097af4a 100644 --- a/lib/src/services/twonly_safe/create_backup.twonly_safe.dart +++ b/lib/src/services/twonly_safe/create_backup.twonly_safe.dart @@ -14,7 +14,7 @@ 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/media_upload.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/storage.dart'; diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 1f1973d..ec33119 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,6 +1,4 @@ -import 'dart:convert'; import 'dart:math'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,14 +9,13 @@ import 'package:local_auth/local_auth.dart'; import 'package:provider/provider.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart'; -import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/utils/log.dart'; extension ShortCutsExtension on BuildContext { AppLocalizations get lang => AppLocalizations.of(this)!; - TwonlyDatabase get db => Provider.of(this); + TwonlyDB get db => Provider.of(this); ColorScheme get color => Theme.of(this).colorScheme; } @@ -245,31 +242,19 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) { return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}'; } -String getMessageText(Message message) { - try { - if (message.contentJson == null) return ''; - return TextMessageContent.fromJson(jsonDecode(message.contentJson!) as Map) - .text; - } catch (e) { - Log.error(e); - return ''; - } -} - -MediaMessageContent? getMediaContent(Message message) { - try { - if (message.contentJson == null) return null; - return MediaMessageContent.fromJson( - jsonDecode(message.contentJson!) as Map, - ); - } catch (e) { - Log.error(e); - return null; - } -} - bool isUUIDNewer(String uuid1, String uuid2) { final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); return timestamp1 > timestamp2; } + +String uint8ListToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); +} + +Uint8List hexToUint8List(String hex) => Uint8List.fromList( + List.generate( + hex.length ~/ 2, + (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16), + ), + ); diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index b4b2e04..119bdd1 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; @@ -14,6 +15,7 @@ Future isUserCreated() async { if (user == null) { return false; } + gUser = user; return true; } @@ -56,7 +58,8 @@ Future updateUserdata( final updated = updateUser(user); await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); - return user; + gUser = updated; + return updated; }); } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index b522422..b10f65f 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -7,8 +7,8 @@ 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/media_upload.dart'; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 26f8d0b..20eb318 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -10,7 +10,9 @@ 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'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -299,17 +301,35 @@ class _CameraPreviewViewState extends State { File? videoFilePath, { bool sharedFromGallery = false, }) async { + final mediaFileService = await initializeMediaUpload( + (videoFilePath != null) ? MediaType.video : MediaType.image, + gUser.defaultShowTime, + ); + if (!mounted) return true; + + if (mediaFileService == null) { + Log.error('Could not generate media file service'); + return false; + } + + if (videoFilePath != null) { + videoFilePath + ..copySync(mediaFileService.originalPath.path) + ..deleteSync(); + + // Start with compressing the video, to speed up the process in case the video is not changed. + unawaited(mediaFileService.compressMedia()); + } + final shouldReturn = await Navigator.push( context, PageRouteBuilder( opaque: false, pageBuilder: (context, a1, a2) => ShareImageEditorView( - videoFilePath: videoFilePath, - imageBytes: imageBytes, + imageBytesFuture: imageBytes, sharedFromGallery: sharedFromGallery, sendTo: widget.sendTo, - mirrorVideo: isFront && Platform.isAndroid && false, - useHighQuality: true, + mediaFileService: mediaFileService, ), transitionsBuilder: (context, animation, secondaryAnimation, child) { return child; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index 3653b9c..94fbd49 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -3,14 +3,19 @@ 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/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -34,32 +39,26 @@ const gMediaShowInfinite = 999999; class ShareImageEditorView extends StatefulWidget { const ShareImageEditorView({ - required this.mirrorVideo, - required this.useHighQuality, required this.sharedFromGallery, + required this.mediaFileService, super.key, - this.imageBytes, + this.imageBytesFuture, this.sendTo, - this.videoFilePath, }); - final Future? imageBytes; - final File? videoFilePath; - final Contact? sendTo; - final bool mirrorVideo; - final bool useHighQuality; + final Future? imageBytesFuture; + final Group? sendTo; final bool sharedFromGallery; + final MediaFileService mediaFileService; @override State createState() => _ShareImageEditorView(); } class _ShareImageEditorView extends State { - bool _isRealTwonly = false; - int maxShowTime = gMediaShowInfinite; double tabDownPosition = 0; bool sendingOrLoadingImage = true; bool loadingImage = true; bool isDisposed = false; - HashSet selectedUserIds = HashSet(); + HashSet selectedGroupIds = HashSet(); double widthRatio = 1; double heightRatio = 1; double pixelRatio = 1; @@ -68,26 +67,31 @@ class _ShareImageEditorView extends State { ScreenshotController screenshotController = ScreenshotController(); /// Media upload variables - int? mediaUploadId; Future? videoUploadHandler; + MediaFileService get mediaService => widget.mediaFileService; + MediaFile get media => widget.mediaFileService.mediaFile; + @override void initState() { super.initState(); - unawaited(initAsync()); - unawaited(initMediaFileUpload()); + layers.add(FilterLayerData()); + if (widget.sendTo != null) { - selectedUserIds.add(widget.sendTo!.userId); + selectedGroupIds.add(widget.sendTo!.groupId); } - if (widget.imageBytes != null) { - unawaited(loadImage(widget.imageBytes!)); - } else if (widget.videoFilePath != null) { + + if (widget.imageBytesFuture != null) { + unawaited(loadImage(widget.imageBytesFuture!)); + } + + if (media.type == MediaType.video) { setState(() { sendingOrLoadingImage = false; loadingImage = false; }); - videoController = VideoPlayerController.file(widget.videoFilePath!); + videoController = VideoPlayerController.file(mediaService.originalPath); videoController?.setLooping(true); videoController?.initialize().then((_) async { await videoController!.play(); @@ -97,29 +101,6 @@ class _ShareImageEditorView extends State { } } - Future initAsync() async { - final user = await getUser(); - if (user == null) return; - if (user.defaultShowTime != null) { - setState(() { - maxShowTime = user.defaultShowTime!; - }); - } - } - - Future initMediaFileUpload() async { - // media init was already called... - if (mediaUploadId != null) return; - - mediaUploadId = await initMediaUpload(); - - if (widget.videoFilePath != null && mediaUploadId != null) { - // start with the video compression... - videoUploadHandler = - addVideoToUpload(mediaUploadId!, widget.videoFilePath!); - } - } - @override void dispose() { isDisposed = true; @@ -128,14 +109,14 @@ class _ShareImageEditorView extends State { super.dispose(); } - void updateStatus(int userId, bool checked) { + void updateSelectedGroupIds(String groupId, bool checked) { if (checked) { - if (_isRealTwonly) { - selectedUserIds.clear(); + if (media.requiresAuthentication) { + selectedGroupIds.clear(); } - selectedUserIds.add(userId); + selectedGroupIds.add(groupId); } else { - selectedUserIds.remove(userId); + selectedGroupIds.remove(groupId); } setState(() {}); } @@ -195,38 +176,36 @@ class _ShareImageEditorView extends State { ), const SizedBox(height: 8), NotificationBadge( - count: (widget.videoFilePath != null) + count: (media.type != MediaType.video) ? '0' - : maxShowTime == gMediaShowInfinite + : media.displayLimitInMilliseconds == null ? '∞' - : maxShowTime.toString(), + : media.displayLimitInMilliseconds.toString(), child: ActionButton( - (widget.videoFilePath != null) - ? maxShowTime == gMediaShowInfinite + (media.type != MediaType.video) + ? media.displayLimitInMilliseconds == null ? Icons.repeat_rounded : Icons.repeat_one_rounded : Icons.timer_outlined, tooltipText: context.lang.protectAsARealTwonly, onPressed: () async { - if (widget.videoFilePath != null) { - setState(() { - if (maxShowTime == gMediaShowInfinite) { - maxShowTime = 0; - } else { - maxShowTime = gMediaShowInfinite; - } - }); + if (media.type != MediaType.video) { + await mediaService.setDisplayLimit( + (media.displayLimitInMilliseconds == null) ? 0 : null); + if (!mounted) return; + setState(() {}); return; } - if (maxShowTime == gMediaShowInfinite) { + int? maxShowTime; + if (media.displayLimitInMilliseconds == null) { maxShowTime = 1; - } else if (maxShowTime == 1) { + } else if (media.displayLimitInMilliseconds == 1) { maxShowTime = 5; - } else if (maxShowTime == 5) { + } else if (media.displayLimitInMilliseconds == 5) { maxShowTime = 20; - } else { - maxShowTime = gMediaShowInfinite; } + await mediaService.setDisplayLimit(maxShowTime); + if (!mounted) return; setState(() {}); await updateUserdata((user) { user.defaultShowTime = maxShowTime; @@ -239,15 +218,12 @@ class _ShareImageEditorView extends State { ActionButton( FontAwesomeIcons.shieldHeart, tooltipText: context.lang.protectAsARealTwonly, - color: _isRealTwonly + color: media.requiresAuthentication ? Theme.of(context).colorScheme.primary : Colors.white, onPressed: () async { - _isRealTwonly = !_isRealTwonly; - if (_isRealTwonly) { - maxShowTime = 12; - } - selectedUserIds = HashSet(); + await mediaService.setRequiresAuth(!media.requiresAuthentication); + selectedGroupIds = HashSet(); setState(() {}); }, ), @@ -308,11 +284,7 @@ class _ShareImageEditorView extends State { } Future pushShareImageView() async { - if (mediaUploadId == null) { - await initMediaFileUpload(); - if (mediaUploadId == null) return; - } - final imageBytes = getMergedImage(); + final imageBytes = storeImageAsOriginal(); await videoController?.pause(); if (isDisposed || !mounted) return; final wasSend = await Navigator.push( @@ -320,13 +292,10 @@ class _ShareImageEditorView extends State { MaterialPageRoute( builder: (context) => ShareImageView( imageBytesFuture: imageBytes, - isRealTwonly: _isRealTwonly, - maxShowTime: maxShowTime, - selectedUserIds: selectedUserIds, - updateStatus: updateStatus, + selectedUserIds: selectedGroupIds, + updateStatus: updateSelectedGroupIds, videoUploadHandler: videoUploadHandler, - mediaUploadId: mediaUploadId!, - mirrorVideo: widget.mirrorVideo, + mediaFileService: mediaService, ), ), ) as bool?; @@ -337,36 +306,46 @@ class _ShareImageEditorView extends State { } } - Future getMergedImage() async { - Uint8List? image; + Future storeImageAsOriginal() async { + if (layers.length == 1) { + if (layers.first is BackgroundLayerData) { + final image = (layers.first as BackgroundLayerData).image.bytes; + mediaService.originalPath.writeAsBytesSync(image); + } + } - - TODO: When changed then create a new mediaID!!!!!! - As storedMediaId would overwrite it.... - - if (layers.length > 1 || widget.videoFilePath != null) { + if (layers.length > 1 || media.type != MediaType.video) { for (final x in layers) { x.showCustomButtons = false; } setState(() {}); - image = await screenshotController.capture( - pixelRatio: (widget.useHighQuality) ? pixelRatio : 1, + final image = await screenshotController.capture( + pixelRatio: pixelRatio, ); + 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); + } + for (final x in layers) { x.showCustomButtons = true; } setState(() {}); - } else if (layers.length == 1) { - if (layers.first is BackgroundLayerData) { - image = (layers.first as BackgroundLayerData).image.bytes; - } } - return image; } - Future loadImage(Future imageFile) async { - final imageBytes = await imageFile; - await currentImage.load(imageBytes); + Future loadImage(Future imageBytesFuture) async { + await currentImage.load(await imageBytesFuture); if (isDisposed) return; if (!context.mounted) return; @@ -388,56 +367,29 @@ class _ShareImageEditorView extends State { setState(() { sendingOrLoadingImage = true; }); - final imageBytes = await getMergedImage(); + + if (media.type == MediaType.image) { + await storeImageAsOriginal(); + } + if (media.type == MediaType.video) { + Log.error('TODO: COMBINE VIDEO AND IMAGE!!!'); + } + if (!context.mounted) return; - if (imageBytes == null) { + + // first finalize the upload + await finalizeUpload(mediaService, [widget.sendTo!.groupId]); + + /// then call the upload process in the background + await encryptMediaFiles( + mediaUploadId!, + imageHandler, + videoUploadHandler, + ); + + if (context.mounted) { // ignore: use_build_context_synchronously Navigator.pop(context, true); - return; - } - final err = await isAllowedToSend(); - if (!context.mounted) return; - - if (err != null) { - setState(() { - sendingOrLoadingImage = false; - }); - if (mounted) { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return SubscriptionView( - redirectError: err, - ); - }, - ), - ); - } - } else { - final imageHandler = addOrModifyImageToUpload(mediaUploadId!, imageBytes); - - // first finalize the upload - await finalizeUpload( - mediaUploadId!, - [widget.sendTo!.userId], - _isRealTwonly, - widget.videoFilePath != null, - widget.mirrorVideo, - maxShowTime, - ); - - /// then call the upload process in the background - await encryptMediaFiles( - mediaUploadId!, - imageHandler, - videoUploadHandler, - ); - - if (context.mounted) { - // ignore: use_build_context_synchronously - Navigator.pop(context, true); - } } } @@ -543,10 +495,7 @@ class _ShareImageEditorView extends State { children: [ if (videoController != null) Positioned.fill( - child: Transform.flip( - flipX: widget.mirrorVideo, - child: VideoPlayer(videoController!), - ), + child: VideoPlayer(videoController!), ), Screenshot( controller: screenshotController, diff --git a/lib/src/views/camera/share_image_view.dart b/lib/src/views/camera/share_image_view.dart index 537a193..22fc367 100644 --- a/lib/src/views/camera/share_image_view.dart +++ b/lib/src/views/camera/share_image_view.dart @@ -9,7 +9,8 @@ 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/twonly.db.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; import 'package:twonly/src/views/components/flame.dart'; @@ -21,25 +22,19 @@ import 'package:twonly/src/views/settings/subscription/subscription.view.dart'; class ShareImageView extends StatefulWidget { const ShareImageView({ required this.imageBytesFuture, - required this.isRealTwonly, - required this.mirrorVideo, - required this.maxShowTime, required this.selectedUserIds, required this.updateStatus, required this.videoUploadHandler, - required this.mediaUploadId, + required this.mediaFileService, super.key, this.enableVideoAudio, }); final Future imageBytesFuture; - final bool isRealTwonly; - final bool mirrorVideo; - final int maxShowTime; final HashSet selectedUserIds; final bool? enableVideoAudio; - final int mediaUploadId; final void Function(int, bool) updateStatus; final Future? videoUploadHandler; + final MediaFileService mediaFileService; @override State createState() => _ShareImageView(); diff --git a/lib/src/views/chats/add_new_user.view.dart b/lib/src/views/chats/add_new_user.view.dart index c05331e..64e5089 100644 --- a/lib/src/views/chats/add_new_user.view.dart +++ b/lib/src/views/chats/add_new_user.view.dart @@ -227,8 +227,7 @@ class ContactsListView extends StatelessWidget { child: IconButton( icon: const Icon(Icons.close, color: Colors.red), onPressed: () async { - await rejectUser(contact.userId); - await deleteContact(contact.userId); + await rejectAndDeleteContact(contact.userId); }, ), ), diff --git a/lib/src/views/chats/chat_list.view.dart b/lib/src/views/chats/chat_list.view.dart index be2444d..f197cd5 100644 --- a/lib/src/views/chats/chat_list.view.dart +++ b/lib/src/views/chats/chat_list.view.dart @@ -12,7 +12,7 @@ import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/providers/connection.provider.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart'; diff --git a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart index 9a247f9..4be5049 100644 --- a/lib/src/views/chats/chat_messages_components/chat_media_entry.dart +++ b/lib/src/views/chats/chat_messages_components/chat_media_entry.dart @@ -8,7 +8,8 @@ import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; -import 'package:twonly/src/services/api/media_download.dart' as received; +import 'package:twonly/src/services/api/mediafiles/download.service.dart' + as received; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index e6650e5..887c1b6 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -15,7 +15,7 @@ import 'package:twonly/src/database/tables/messages_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/push_notification/push_notification.pb.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/services/api/messages.dart'; import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart'; diff --git a/lib/src/views/contact/contact.view.dart b/lib/src/views/contact/contact.view.dart index 52d2ef3..d08a914 100644 --- a/lib/src/views/contact/contact.view.dart +++ b/lib/src/views/contact/contact.view.dart @@ -31,8 +31,7 @@ class _ContactViewState extends State { ); if (remove) { // trigger deletion for the other user... - await rejectUser(contact.userId); - await deleteContact(contact.userId); + await rejectAndDeleteContact(contact.userId); if (mounted) { Navigator.popUntil(context, (route) => route.isFirst); } diff --git a/lib/src/views/memories/memories.view.dart b/lib/src/views/memories/memories.view.dart index 588fdfe..fd95f31 100644 --- a/lib/src/views/memories/memories.view.dart +++ b/lib/src/views/memories/memories.view.dart @@ -6,8 +6,8 @@ import 'package:intl/intl.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/media_upload.dart' as send; -import 'package:twonly/src/services/thumbnail.service.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; +import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; diff --git a/lib/src/views/memories/memories_photo_slider.view.dart b/lib/src/views/memories/memories_photo_slider.view.dart index 5489e9b..bb2abf5 100644 --- a/lib/src/views/memories/memories_photo_slider.view.dart +++ b/lib/src/views/memories/memories_photo_slider.view.dart @@ -6,8 +6,9 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/model/memory_item.model.dart'; -import 'package:twonly/src/services/api/media_download.dart' as received; -import 'package:twonly/src/services/api/media_upload.dart' as send; +import 'package:twonly/src/services/api/mediafiles/download.service.dart' + as received; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index 84edc97..8e8d2af 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -90,6 +90,8 @@ class _RegisterViewState extends State { await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); + gUser = userData; + await apiService.authenticate(); widget.callbackOnSuccess(); } diff --git a/lib/src/views/settings/data_and_storage.view.dart b/lib/src/views/settings/data_and_storage.view.dart index 4c21c75..5f5b964 100644 --- a/lib/src/views/settings/data_and_storage.view.dart +++ b/lib/src/views/settings/data_and_storage.view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/services/api/media_download.dart'; +import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; diff --git a/lib/src/views/settings/help/contact_us.view.dart b/lib/src/views/settings/help/contact_us.view.dart index 6d30400..ad6a570 100644 --- a/lib/src/views/settings/help/contact_us.view.dart +++ b/lib/src/views/settings/help/contact_us.view.dart @@ -8,7 +8,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; -import 'package:twonly/src/services/api/media_upload.dart' +import 'package:twonly/src/services/api/mediafiles/upload.service.dart' show createDownloadToken, uint8ListToHex; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; diff --git a/test/unit_test.dart b/test/unit_test.dart index 8d4c715..ffe3b19 100644 --- a/test/unit_test.dart +++ b/test/unit_test.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:hashlib/random.dart'; -import 'package:twonly/src/services/api/media_upload.dart'; +import 'package:twonly/src/services/api/mediafiles/upload.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/views/components/animate_icon.dart';