starting with rewriting the upload process

This commit is contained in:
otsmr 2025-10-23 00:35:28 +02:00
parent f5cbcf154b
commit b2dc384465
36 changed files with 557 additions and 648 deletions

View file

@ -6,7 +6,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/localization/generated/app_localizations.dart'; import 'package:twonly/src/localization/generated/app_localizations.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/providers/settings.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/utils/storage.dart';
import 'package:twonly/src/views/components/app_outdated.dart'; import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart'; import 'package:twonly/src/views/home.view.dart';

View file

@ -1,5 +1,6 @@
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api.service.dart';
late ApiService apiService; late ApiService apiService;
@ -9,6 +10,10 @@ late TwonlyDB twonlyDB;
List<CameraDescription> gCameras = <CameraDescription>[]; List<CameraDescription> gCameras = <CameraDescription>[];
// 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 following global function can be called from anywhere to update
// the UI when something changed. The callbacks will be set by // the UI when something changed. The callbacks will be set by
// App widget. // App widget.

View file

@ -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/image_editor.provider.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api.service.dart';
import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.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/fcm.service.dart'; import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart';
import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart'; import 'package:twonly/src/services/twonly_safe/create_backup.twonly_safe.dart';
@ -28,6 +28,7 @@ void main() async {
final user = await getUser(); final user = await getUser();
if (user != null) { if (user != null) {
gUser = user;
if (user.isDemoUser) { if (user.isDemoUser) {
await deleteLocalUserData(); await deleteLocalUserData();
} }

View file

@ -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/mediafiles.table.dart';
import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/tables/messages.table.dart';
import 'package:twonly/src/database/twonly.db.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'; import 'package:twonly/src/utils/log.dart';
part 'messages.dao.g.dart'; part 'messages.dao.g.dart';

View file

@ -10,10 +10,21 @@ enum MediaType {
} }
enum UploadState { enum UploadState {
pending, // Image/Video was taken. A database entry was created to track it...
readyToUpload, initialized,
uploadTaskStarted, // Image was stored but not send
receiverNotified, 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 { enum DownloadState {
@ -33,7 +44,8 @@ class MediaFiles extends Table {
TextColumn get uploadState => textEnum<UploadState>().nullable()(); TextColumn get uploadState => textEnum<UploadState>().nullable()();
TextColumn get downloadState => textEnum<DownloadState>().nullable()(); TextColumn get downloadState => textEnum<DownloadState>().nullable()();
BoolColumn get requiresAuthentication => boolean()(); BoolColumn get requiresAuthentication =>
boolean().withDefault(const Constant(false))();
BoolColumn get reopenByContact => BoolColumn get reopenByContact =>
boolean().withDefault(const Constant(false))(); boolean().withDefault(const Constant(false))();

View file

@ -18,6 +18,8 @@ class Messages extends Table {
TextColumn get mediaId => TextColumn get mediaId =>
text().nullable().references(MediaFiles, #mediaId)(); text().nullable().references(MediaFiles, #mediaId)();
BlobColumn get downloadToken => blob().nullable()();
TextColumn get quotesMessageId => TextColumn get quotesMessageId =>
text().nullable().references(Messages, #messageId)(); text().nullable().references(Messages, #messageId)();

View file

@ -1460,9 +1460,10 @@ class $MediaFilesTable extends MediaFiles
late final GeneratedColumn<bool> requiresAuthentication = late final GeneratedColumn<bool> requiresAuthentication =
GeneratedColumn<bool>('requires_authentication', aliasedName, false, GeneratedColumn<bool>('requires_authentication', aliasedName, false,
type: DriftSqlType.bool, type: DriftSqlType.bool,
requiredDuringInsert: true, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("requires_authentication" IN (0, 1))')); 'CHECK ("requires_authentication" IN (0, 1))'),
defaultValue: const Constant(false));
static const VerificationMeta _reopenByContactMeta = static const VerificationMeta _reopenByContactMeta =
const VerificationMeta('reopenByContact'); const VerificationMeta('reopenByContact');
@override @override
@ -1563,8 +1564,6 @@ class $MediaFilesTable extends MediaFiles
_requiresAuthenticationMeta, _requiresAuthenticationMeta,
requiresAuthentication.isAcceptableOrUnknown( requiresAuthentication.isAcceptableOrUnknown(
data['requires_authentication']!, _requiresAuthenticationMeta)); data['requires_authentication']!, _requiresAuthenticationMeta));
} else if (isInserting) {
context.missing(_requiresAuthenticationMeta);
} }
if (data.containsKey('reopen_by_contact')) { if (data.containsKey('reopen_by_contact')) {
context.handle( context.handle(
@ -2017,7 +2016,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
required MediaType type, required MediaType type,
this.uploadState = const Value.absent(), this.uploadState = const Value.absent(),
this.downloadState = const Value.absent(), this.downloadState = const Value.absent(),
required bool requiresAuthentication, this.requiresAuthentication = const Value.absent(),
this.reopenByContact = const Value.absent(), this.reopenByContact = const Value.absent(),
this.stored = const Value.absent(), this.stored = const Value.absent(),
this.reuploadRequestedBy = const Value.absent(), this.reuploadRequestedBy = const Value.absent(),
@ -2028,8 +2027,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.encryptionNonce = const Value.absent(), this.encryptionNonce = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : type = Value(type), }) : type = Value(type);
requiresAuthentication = Value(requiresAuthentication);
static Insertable<MediaFile> custom({ static Insertable<MediaFile> custom({
Expression<String>? mediaId, Expression<String>? mediaId,
Expression<String>? type, Expression<String>? type,
@ -2233,6 +2231,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways( defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES media_files (media_id)')); 'REFERENCES media_files (media_id)'));
static const VerificationMeta _downloadTokenMeta =
const VerificationMeta('downloadToken');
@override
late final GeneratedColumn<Uint8List> downloadToken =
GeneratedColumn<Uint8List>('download_token', aliasedName, true,
type: DriftSqlType.blob, requiredDuringInsert: false);
static const VerificationMeta _quotesMessageIdMeta = static const VerificationMeta _quotesMessageIdMeta =
const VerificationMeta('quotesMessageId'); const VerificationMeta('quotesMessageId');
@override @override
@ -2319,6 +2323,7 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
senderId, senderId,
content, content,
mediaId, mediaId,
downloadToken,
quotesMessageId, quotesMessageId,
isDeletedFromSender, isDeletedFromSender,
isEdited, isEdited,
@ -2361,6 +2366,12 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
context.handle(_mediaIdMeta, context.handle(_mediaIdMeta,
mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta)); mediaId.isAcceptableOrUnknown(data['media_id']!, _mediaIdMeta));
} }
if (data.containsKey('download_token')) {
context.handle(
_downloadTokenMeta,
downloadToken.isAcceptableOrUnknown(
data['download_token']!, _downloadTokenMeta));
}
if (data.containsKey('quotes_message_id')) { if (data.containsKey('quotes_message_id')) {
context.handle( context.handle(
_quotesMessageIdMeta, _quotesMessageIdMeta,
@ -2428,6 +2439,8 @@ class $MessagesTable extends Messages with TableInfo<$MessagesTable, Message> {
.read(DriftSqlType.string, data['${effectivePrefix}content']), .read(DriftSqlType.string, data['${effectivePrefix}content']),
mediaId: attachedDatabase.typeMapping mediaId: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}media_id']), .read(DriftSqlType.string, data['${effectivePrefix}media_id']),
downloadToken: attachedDatabase.typeMapping
.read(DriftSqlType.blob, data['${effectivePrefix}download_token']),
quotesMessageId: attachedDatabase.typeMapping.read( quotesMessageId: attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}quotes_message_id']), DriftSqlType.string, data['${effectivePrefix}quotes_message_id']),
isDeletedFromSender: attachedDatabase.typeMapping.read( isDeletedFromSender: attachedDatabase.typeMapping.read(
@ -2461,6 +2474,7 @@ class Message extends DataClass implements Insertable<Message> {
final int? senderId; final int? senderId;
final String? content; final String? content;
final String? mediaId; final String? mediaId;
final Uint8List? downloadToken;
final String? quotesMessageId; final String? quotesMessageId;
final bool isDeletedFromSender; final bool isDeletedFromSender;
final bool isEdited; final bool isEdited;
@ -2476,6 +2490,7 @@ class Message extends DataClass implements Insertable<Message> {
this.senderId, this.senderId,
this.content, this.content,
this.mediaId, this.mediaId,
this.downloadToken,
this.quotesMessageId, this.quotesMessageId,
required this.isDeletedFromSender, required this.isDeletedFromSender,
required this.isEdited, required this.isEdited,
@ -2499,6 +2514,9 @@ class Message extends DataClass implements Insertable<Message> {
if (!nullToAbsent || mediaId != null) { if (!nullToAbsent || mediaId != null) {
map['media_id'] = Variable<String>(mediaId); map['media_id'] = Variable<String>(mediaId);
} }
if (!nullToAbsent || downloadToken != null) {
map['download_token'] = Variable<Uint8List>(downloadToken);
}
if (!nullToAbsent || quotesMessageId != null) { if (!nullToAbsent || quotesMessageId != null) {
map['quotes_message_id'] = Variable<String>(quotesMessageId); map['quotes_message_id'] = Variable<String>(quotesMessageId);
} }
@ -2530,6 +2548,9 @@ class Message extends DataClass implements Insertable<Message> {
mediaId: mediaId == null && nullToAbsent mediaId: mediaId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(mediaId), : Value(mediaId),
downloadToken: downloadToken == null && nullToAbsent
? const Value.absent()
: Value(downloadToken),
quotesMessageId: quotesMessageId == null && nullToAbsent quotesMessageId: quotesMessageId == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(quotesMessageId), : Value(quotesMessageId),
@ -2557,6 +2578,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId: serializer.fromJson<int?>(json['senderId']), senderId: serializer.fromJson<int?>(json['senderId']),
content: serializer.fromJson<String?>(json['content']), content: serializer.fromJson<String?>(json['content']),
mediaId: serializer.fromJson<String?>(json['mediaId']), mediaId: serializer.fromJson<String?>(json['mediaId']),
downloadToken: serializer.fromJson<Uint8List?>(json['downloadToken']),
quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']), quotesMessageId: serializer.fromJson<String?>(json['quotesMessageId']),
isDeletedFromSender: isDeletedFromSender:
serializer.fromJson<bool>(json['isDeletedFromSender']), serializer.fromJson<bool>(json['isDeletedFromSender']),
@ -2578,6 +2600,7 @@ class Message extends DataClass implements Insertable<Message> {
'senderId': serializer.toJson<int?>(senderId), 'senderId': serializer.toJson<int?>(senderId),
'content': serializer.toJson<String?>(content), 'content': serializer.toJson<String?>(content),
'mediaId': serializer.toJson<String?>(mediaId), 'mediaId': serializer.toJson<String?>(mediaId),
'downloadToken': serializer.toJson<Uint8List?>(downloadToken),
'quotesMessageId': serializer.toJson<String?>(quotesMessageId), 'quotesMessageId': serializer.toJson<String?>(quotesMessageId),
'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender), 'isDeletedFromSender': serializer.toJson<bool>(isDeletedFromSender),
'isEdited': serializer.toJson<bool>(isEdited), 'isEdited': serializer.toJson<bool>(isEdited),
@ -2596,6 +2619,7 @@ class Message extends DataClass implements Insertable<Message> {
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(), Value<String?> quotesMessageId = const Value.absent(),
bool? isDeletedFromSender, bool? isDeletedFromSender,
bool? isEdited, bool? isEdited,
@ -2611,6 +2635,8 @@ class Message extends DataClass implements Insertable<Message> {
senderId: senderId.present ? senderId.value : this.senderId, senderId: senderId.present ? senderId.value : this.senderId,
content: content.present ? content.value : this.content, content: content.present ? content.value : this.content,
mediaId: mediaId.present ? mediaId.value : this.mediaId, mediaId: mediaId.present ? mediaId.value : this.mediaId,
downloadToken:
downloadToken.present ? downloadToken.value : this.downloadToken,
quotesMessageId: quotesMessageId.present quotesMessageId: quotesMessageId.present
? quotesMessageId.value ? quotesMessageId.value
: this.quotesMessageId, : this.quotesMessageId,
@ -2630,6 +2656,9 @@ class Message extends DataClass implements Insertable<Message> {
senderId: data.senderId.present ? data.senderId.value : this.senderId, senderId: data.senderId.present ? data.senderId.value : this.senderId,
content: data.content.present ? data.content.value : this.content, content: data.content.present ? data.content.value : this.content,
mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId, mediaId: data.mediaId.present ? data.mediaId.value : this.mediaId,
downloadToken: data.downloadToken.present
? data.downloadToken.value
: this.downloadToken,
quotesMessageId: data.quotesMessageId.present quotesMessageId: data.quotesMessageId.present
? data.quotesMessageId.value ? data.quotesMessageId.value
: this.quotesMessageId, : this.quotesMessageId,
@ -2658,6 +2687,7 @@ class Message extends DataClass implements Insertable<Message> {
..write('senderId: $senderId, ') ..write('senderId: $senderId, ')
..write('content: $content, ') ..write('content: $content, ')
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ') ..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isDeletedFromSender: $isDeletedFromSender, ')
..write('isEdited: $isEdited, ') ..write('isEdited: $isEdited, ')
@ -2678,6 +2708,7 @@ class Message extends DataClass implements Insertable<Message> {
senderId, senderId,
content, content,
mediaId, mediaId,
$driftBlobEquality.hash(downloadToken),
quotesMessageId, quotesMessageId,
isDeletedFromSender, isDeletedFromSender,
isEdited, isEdited,
@ -2696,6 +2727,7 @@ class Message extends DataClass implements Insertable<Message> {
other.senderId == this.senderId && other.senderId == this.senderId &&
other.content == this.content && other.content == this.content &&
other.mediaId == this.mediaId && other.mediaId == this.mediaId &&
$driftBlobEquality.equals(other.downloadToken, this.downloadToken) &&
other.quotesMessageId == this.quotesMessageId && other.quotesMessageId == this.quotesMessageId &&
other.isDeletedFromSender == this.isDeletedFromSender && other.isDeletedFromSender == this.isDeletedFromSender &&
other.isEdited == this.isEdited && other.isEdited == this.isEdited &&
@ -2713,6 +2745,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
final Value<int?> senderId; final Value<int?> senderId;
final Value<String?> content; final Value<String?> content;
final Value<String?> mediaId; final Value<String?> mediaId;
final Value<Uint8List?> downloadToken;
final Value<String?> quotesMessageId; final Value<String?> quotesMessageId;
final Value<bool> isDeletedFromSender; final Value<bool> isDeletedFromSender;
final Value<bool> isEdited; final Value<bool> isEdited;
@ -2729,6 +2762,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(), this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(), this.isDeletedFromSender = const Value.absent(),
this.isEdited = const Value.absent(), this.isEdited = const Value.absent(),
@ -2746,6 +2780,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
this.senderId = const Value.absent(), this.senderId = const Value.absent(),
this.content = const Value.absent(), this.content = const Value.absent(),
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.downloadToken = const Value.absent(),
this.quotesMessageId = const Value.absent(), this.quotesMessageId = const Value.absent(),
this.isDeletedFromSender = const Value.absent(), this.isDeletedFromSender = const Value.absent(),
this.isEdited = const Value.absent(), this.isEdited = const Value.absent(),
@ -2763,6 +2798,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Expression<int>? senderId, Expression<int>? senderId,
Expression<String>? content, Expression<String>? content,
Expression<String>? mediaId, Expression<String>? mediaId,
Expression<Uint8List>? downloadToken,
Expression<String>? quotesMessageId, Expression<String>? quotesMessageId,
Expression<bool>? isDeletedFromSender, Expression<bool>? isDeletedFromSender,
Expression<bool>? isEdited, Expression<bool>? isEdited,
@ -2780,6 +2816,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (senderId != null) 'sender_id': senderId, if (senderId != null) 'sender_id': senderId,
if (content != null) 'content': content, if (content != null) 'content': content,
if (mediaId != null) 'media_id': mediaId, if (mediaId != null) 'media_id': mediaId,
if (downloadToken != null) 'download_token': downloadToken,
if (quotesMessageId != null) 'quotes_message_id': quotesMessageId, if (quotesMessageId != null) 'quotes_message_id': quotesMessageId,
if (isDeletedFromSender != null) if (isDeletedFromSender != null)
'is_deleted_from_sender': isDeletedFromSender, 'is_deleted_from_sender': isDeletedFromSender,
@ -2800,6 +2837,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
Value<int?>? senderId, Value<int?>? senderId,
Value<String?>? content, Value<String?>? content,
Value<String?>? mediaId, Value<String?>? mediaId,
Value<Uint8List?>? downloadToken,
Value<String?>? quotesMessageId, Value<String?>? quotesMessageId,
Value<bool>? isDeletedFromSender, Value<bool>? isDeletedFromSender,
Value<bool>? isEdited, Value<bool>? isEdited,
@ -2816,6 +2854,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
senderId: senderId ?? this.senderId, senderId: senderId ?? this.senderId,
content: content ?? this.content, content: content ?? this.content,
mediaId: mediaId ?? this.mediaId, mediaId: mediaId ?? this.mediaId,
downloadToken: downloadToken ?? this.downloadToken,
quotesMessageId: quotesMessageId ?? this.quotesMessageId, quotesMessageId: quotesMessageId ?? this.quotesMessageId,
isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender, isDeletedFromSender: isDeletedFromSender ?? this.isDeletedFromSender,
isEdited: isEdited ?? this.isEdited, isEdited: isEdited ?? this.isEdited,
@ -2847,6 +2886,9 @@ class MessagesCompanion extends UpdateCompanion<Message> {
if (mediaId.present) { if (mediaId.present) {
map['media_id'] = Variable<String>(mediaId.value); map['media_id'] = Variable<String>(mediaId.value);
} }
if (downloadToken.present) {
map['download_token'] = Variable<Uint8List>(downloadToken.value);
}
if (quotesMessageId.present) { if (quotesMessageId.present) {
map['quotes_message_id'] = Variable<String>(quotesMessageId.value); map['quotes_message_id'] = Variable<String>(quotesMessageId.value);
} }
@ -2888,6 +2930,7 @@ class MessagesCompanion extends UpdateCompanion<Message> {
..write('senderId: $senderId, ') ..write('senderId: $senderId, ')
..write('content: $content, ') ..write('content: $content, ')
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('downloadToken: $downloadToken, ')
..write('quotesMessageId: $quotesMessageId, ') ..write('quotesMessageId: $quotesMessageId, ')
..write('isDeletedFromSender: $isDeletedFromSender, ') ..write('isDeletedFromSender: $isDeletedFromSender, ')
..write('isEdited: $isEdited, ') ..write('isEdited: $isEdited, ')
@ -7100,7 +7143,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({
required MediaType type, required MediaType type,
Value<UploadState?> uploadState, Value<UploadState?> uploadState,
Value<DownloadState?> downloadState, Value<DownloadState?> downloadState,
required bool requiresAuthentication, Value<bool> requiresAuthentication,
Value<bool> reopenByContact, Value<bool> reopenByContact,
Value<bool> stored, Value<bool> stored,
Value<List<int>?> reuploadRequestedBy, Value<List<int>?> reuploadRequestedBy,
@ -7433,7 +7476,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
required MediaType type, required MediaType type,
Value<UploadState?> uploadState = const Value.absent(), Value<UploadState?> uploadState = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
required bool requiresAuthentication, Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = const Value.absent(), Value<bool> reopenByContact = const Value.absent(),
Value<bool> stored = const Value.absent(), Value<bool> stored = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(), Value<List<int>?> reuploadRequestedBy = const Value.absent(),
@ -7513,6 +7556,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({
Value<int?> senderId, Value<int?> senderId,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId, Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender, Value<bool> isDeletedFromSender,
Value<bool> isEdited, Value<bool> isEdited,
@ -7530,6 +7574,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({
Value<int?> senderId, Value<int?> senderId,
Value<String?> content, Value<String?> content,
Value<String?> mediaId, Value<String?> mediaId,
Value<Uint8List?> downloadToken,
Value<String?> quotesMessageId, Value<String?> quotesMessageId,
Value<bool> isDeletedFromSender, Value<bool> isDeletedFromSender,
Value<bool> isEdited, Value<bool> isEdited,
@ -7671,6 +7716,9 @@ class $$MessagesTableFilterComposer
ColumnFilters<String> get content => $composableBuilder( ColumnFilters<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnFilters(column)); column: $table.content, builder: (column) => ColumnFilters(column));
ColumnFilters<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get isDeletedFromSender => $composableBuilder( ColumnFilters<bool> get isDeletedFromSender => $composableBuilder(
column: $table.isDeletedFromSender, column: $table.isDeletedFromSender,
builder: (column) => ColumnFilters(column)); builder: (column) => ColumnFilters(column));
@ -7856,6 +7904,10 @@ class $$MessagesTableOrderingComposer
ColumnOrderings<String> get content => $composableBuilder( ColumnOrderings<String> get content => $composableBuilder(
column: $table.content, builder: (column) => ColumnOrderings(column)); column: $table.content, builder: (column) => ColumnOrderings(column));
ColumnOrderings<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get isDeletedFromSender => $composableBuilder( ColumnOrderings<bool> get isDeletedFromSender => $composableBuilder(
column: $table.isDeletedFromSender, column: $table.isDeletedFromSender,
builder: (column) => ColumnOrderings(column)); builder: (column) => ColumnOrderings(column));
@ -7978,6 +8030,9 @@ class $$MessagesTableAnnotationComposer
GeneratedColumn<String> get content => GeneratedColumn<String> get content =>
$composableBuilder(column: $table.content, builder: (column) => column); $composableBuilder(column: $table.content, builder: (column) => column);
GeneratedColumn<Uint8List> get downloadToken => $composableBuilder(
column: $table.downloadToken, builder: (column) => column);
GeneratedColumn<bool> get isDeletedFromSender => $composableBuilder( GeneratedColumn<bool> get isDeletedFromSender => $composableBuilder(
column: $table.isDeletedFromSender, builder: (column) => column); column: $table.isDeletedFromSender, builder: (column) => column);
@ -8181,6 +8236,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(), Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(), Value<bool> isDeletedFromSender = const Value.absent(),
Value<bool> isEdited = const Value.absent(), Value<bool> isEdited = const Value.absent(),
@ -8198,6 +8254,7 @@ class $$MessagesTableTableManager extends RootTableManager<
senderId: senderId, senderId: senderId,
content: content, content: content,
mediaId: mediaId, mediaId: mediaId,
downloadToken: downloadToken,
quotesMessageId: quotesMessageId, quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender, isDeletedFromSender: isDeletedFromSender,
isEdited: isEdited, isEdited: isEdited,
@ -8215,6 +8272,7 @@ class $$MessagesTableTableManager extends RootTableManager<
Value<int?> senderId = const Value.absent(), Value<int?> senderId = const Value.absent(),
Value<String?> content = const Value.absent(), Value<String?> content = const Value.absent(),
Value<String?> mediaId = const Value.absent(), Value<String?> mediaId = const Value.absent(),
Value<Uint8List?> downloadToken = const Value.absent(),
Value<String?> quotesMessageId = const Value.absent(), Value<String?> quotesMessageId = const Value.absent(),
Value<bool> isDeletedFromSender = const Value.absent(), Value<bool> isDeletedFromSender = const Value.absent(),
Value<bool> isEdited = const Value.absent(), Value<bool> isEdited = const Value.absent(),
@ -8232,6 +8290,7 @@ class $$MessagesTableTableManager extends RootTableManager<
senderId: senderId, senderId: senderId,
content: content, content: content,
mediaId: mediaId, mediaId: mediaId,
downloadToken: downloadToken,
quotesMessageId: quotesMessageId, quotesMessageId: quotesMessageId,
isDeletedFromSender: isDeletedFromSender, isDeletedFromSender: isDeletedFromSender,
isEdited: isEdited, isEdited: isEdited,

View file

@ -5,8 +5,8 @@ import 'package:drift/drift.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/services/api/media_upload.dart' as send; import 'package:twonly/src/services/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
class MemoryItem { class MemoryItem {
MemoryItem({ MemoryItem({

View file

@ -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' import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server; as server;
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart'; import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pbserver.dart';
import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.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/api/messages.dart'; import 'package:twonly/src/services/api/messages.dart';
import 'package:twonly/src/services/api/server_messages.dart'; import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';

View file

@ -12,10 +12,10 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pbserver.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/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/log.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
Future<void> tryDownloadAllMediaFiles({bool force = false}) async { Future<void> tryDownloadAllMediaFiles({bool force = false}) async {

View file

@ -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<void> 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,
);
}
}

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart'; import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
@ -17,14 +16,13 @@ import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/database/tables/media_uploads_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/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.dart'; import 'package:twonly/src/model/json/message_old.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart'; import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.dart';
import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/model/protobuf/push_notification/push_notification.pbserver.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart';
import 'package:twonly/src/services/api/media_download.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/signal/encryption.signal.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/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:twonly/src/utils/storage.dart';
import 'package:video_compress/video_compress.dart'; import 'package:video_compress/video_compress.dart';
Future<ErrorCode?> 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<void> 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: /// States:
/// when user recorded an video /// when user recorded an video
/// 1. Compress video /// 1. Compress video
@ -115,43 +43,43 @@ Future<void> initFileDownloader() async {
/// Create a new entry in the database /// Create a new entry in the database
Future<bool> checkForFailedUploads() async { // Future<bool> checkForFailedUploads() async {
final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload(); // final messages = await twonlyDB.messagesDao.getAllMessagesPendingUpload();
final mediaUploadIds = <int>[]; // final mediaUploadIds = <int>[];
for (final message in messages) { // for (final message in messages) {
if (mediaUploadIds.contains(message.mediaUploadId)) { // if (mediaUploadIds.contains(message.mediaUploadId)) {
continue; // continue;
} // }
final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload( // final affectedRows = await twonlyDB.mediaUploadsDao.updateMediaUpload(
message.mediaUploadId!, // message.mediaUploadId!,
const MediaUploadsCompanion( // const MediaUploadsCompanion(
state: Value(UploadState.pending), // state: Value(UploadState.pending),
encryptionData: Value( // encryptionData: Value(
null, // start from scratch e.q. encrypt the files again if already happen // null, // start from scratch e.q. encrypt the files again if already happen
), // ),
), // ),
); // );
if (affectedRows == 0) { // if (affectedRows == 0) {
Log.error( // Log.error(
'The media from message ${message.messageId} already deleted.', // 'The media from message ${message.messageId} already deleted.',
); // );
await twonlyDB.messagesDao.updateMessageByMessageId( // await twonlyDB.messagesDao.updateMessageByMessageId(
message.messageId, // message.messageId,
const MessagesCompanion( // const MessagesCompanion(
errorWhileSending: Value(true), // errorWhileSending: Value(true),
), // ),
); // );
} else { // } else {
mediaUploadIds.add(message.mediaUploadId!); // mediaUploadIds.add(message.mediaUploadId!);
} // }
} // }
if (messages.isNotEmpty) { // if (messages.isNotEmpty) {
Log.error( // Log.error(
'Got ${messages.length} messages (${mediaUploadIds.length} media upload files) that are not correctly uploaded. Trying from scratch again.', // '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 // return mediaUploadIds.isNotEmpty; // return true if there are affected
} // }
final lockingHandleMediaFile = Mutex(); final lockingHandleMediaFile = Mutex();
Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async { Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
@ -192,82 +120,25 @@ Future<void> retryMediaUpload(bool appRestarted, {int maxRetries = 3}) async {
} }
} }
Future<int?> initMediaUpload() async { Future<MediaFileService?> initializeMediaUpload(
return twonlyDB.mediaUploadsDao MediaType type,
.insertMediaUpload(const MediaUploadsCompanion()); int? displayLimitInMilliseconds,
}
Future<bool> addVideoToUpload(int mediaUploadId, File videoFilePath) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
await videoFilePath.copy('$basePath.original.mp4');
return compressVideoIfExists(mediaUploadId);
}
Future<Uint8List> addOrModifyImageToUpload(
int mediaUploadId,
Uint8List imageBytes,
) async { ) async {
Uint8List imageBytesCompressed; final chacha20 = FlutterChacha20.poly1305Aead();
final encryptionKey = await (await chacha20.newSecretKey()).extract();
final encryptionNonce = chacha20.newNonce();
final stopwatch = Stopwatch()..start(); final mediaFile = await twonlyDB.mediaFilesDao.insertMedia(
MediaFilesCompanion(
Log.info('Raw images size in bytes: ${imageBytes.length}'); uploadState: const Value(UploadState.initialized),
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
try { encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)),
imageBytesCompressed = await FlutterImageCompress.compressWithList( encryptionNonce: Value(Uint8List.fromList(encryptionNonce)),
format: CompressFormat.webp, type: Value(type),
// 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),
), ),
); );
return imageBytesCompressed; if (mediaFile == null) return null;
return MediaFileService.fromMedia(mediaFile);
} }
Future<void> handlePreProcessingState(MediaUpload media) async { Future<void> handlePreProcessingState(MediaUpload media) async {
@ -304,7 +175,6 @@ Future<void> encryptMediaFiles(
final state = MediaEncryptionData(); final state = MediaEncryptionData();
final chacha20 = FlutterChacha20.poly1305Aead(); final chacha20 = FlutterChacha20.poly1305Aead();
final secretKey = await (await chacha20.newSecretKey()).extract();
state state
..encryptionKey = secretKey.bytes ..encryptionKey = secretKey.bytes
@ -338,70 +208,37 @@ Future<void> encryptMediaFiles(
} }
Future<void> finalizeUpload( Future<void> finalizeUpload(
int mediaUploadId, MediaFileService mediaService,
List<int> contactIds, List<String> groupIds,
bool isRealTwonly,
bool isVideo,
bool mirrorVideo,
int maxShowTime,
) async { ) async {
final metadata = MediaUploadMetadata() final messageIds = <Message>[];
..contactIds = contactIds
..isRealTwonly = isRealTwonly
..messageSendAt = DateTime.now()
..isVideo = isVideo
..maxShowTime = maxShowTime
..mirrorVideo = mirrorVideo;
final messageIds = <int>[]; for (final groupId in groupIds) {
final message = await twonlyDB.messagesDao.insertMessage(
for (final contactId in contactIds) {
final messageId = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion( MessagesCompanion(
contactId: Value(contactId), groupId: Value(groupId),
kind: const Value(MessageKind.media), mediaId: Value(mediaService.mediaFile.mediaId),
sendAt: Value(metadata.messageSendAt), ),
downloadState: const Value(DownloadState.pending), );
mediaUploadId: Value(mediaUploadId), if (message != null) {
contentJson: Value( messageIds.add(message);
jsonEncode( // de-archive contact when sending a new message
MediaMessageContent( await twonlyDB.groupsDao.updateGroup(
maxShowTime: maxShowTime, message.groupId,
isRealTwonly: isRealTwonly, const GroupsCompanion(
isVideo: isVideo, archived: Value(false),
mirrorVideo: mirrorVideo,
).toJson(),
),
), ),
), );
);
// 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 { } else {
Log.error('Error inserting media upload message in database.'); Log.error('Error inserting media upload message in database.');
} }
} }
await twonlyDB.mediaUploadsDao.updateMediaUpload( unawaited(handleNextMediaUploadSteps(mediaService.mediaFile.mediaId));
mediaUploadId,
MediaUploadsCompanion(
messageIds: Value(messageIds),
metadata: Value(metadata),
),
);
unawaited(handleNextMediaUploadSteps(mediaUploadId));
} }
final lockingHandleNextMediaUploadStep = Mutex(); final lockingHandleNextMediaUploadStep = Mutex();
Future<void> handleNextMediaUploadSteps(int mediaUploadId) async { Future<void> handleNextMediaUploadSteps(String mediaUploadId) async {
await lockingHandleNextMediaUploadStep.protect(() async { await lockingHandleNextMediaUploadStep.protect(() async {
final mediaUpload = await twonlyDB.mediaUploadsDao final mediaUpload = await twonlyDB.mediaUploadsDao
.getMediaUploadById(mediaUploadId) .getMediaUploadById(mediaUploadId)
@ -549,7 +386,7 @@ Future<void> handleMediaUpload(MediaUpload media) async {
continue; continue;
} }
final downloadToken = createDownloadToken(); final downloadToken = getRandomUint8List(32);
final msg = MessageJson( final msg = MessageJson(
kind: MessageKind.media, kind: MessageKind.media,
@ -734,159 +571,14 @@ Future<void> uploadFileFast(
Log.info('Upload successful!'); Log.info('Upload successful!');
await handleUploadSuccess(media); await handleUploadSuccess(media);
return; return;
} else if (response.statusCode == 429) {
await twonlyDB.mediaFilesDao.updateMedia(
media.mediaId,
const MediaFilesCompanion(
uploadState: Value(UploadState.uploadLimitReached),
),
);
} else { } else {
Log.info('Upload failed with status: ${response.statusCode}'); Log.info('Upload failed with status: ${response.statusCode}');
} }
} }
Future<bool> 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<Uint8List> 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<File> 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<void> deleteSendMediaFile(int mediaUploadId, String type) async {
final basePath = await getMediaFilePath(mediaUploadId, 'send');
final file = File('$basePath.$type');
if (file.existsSync()) {
await file.delete();
}
}
Future<String> 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<String> 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<Uint8List> 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<void> purgeSendMediaFiles() async {
final basedir = await getApplicationSupportDirectory();
final directory = Directory(join(basedir.path, 'media', 'send'));
await purgeMediaFiles(directory);
}
String uint8ListToHex(List<int> bytes) {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
List<int>.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;
}

View file

@ -4,9 +4,9 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/client/generated/messages.pb.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/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'; import 'package:twonly/src/utils/log.dart';
Future<void> handleMedia( Future<void> handleMedia(
@ -157,7 +157,7 @@ Future<void> handleMediaUpdate(
await twonlyDB.mediaFilesDao.updateMedia( await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId, mediaFile.mediaId,
MediaFilesCompanion( MediaFilesCompanion(
uploadState: const Value(UploadState.pending), uploadState: const Value(UploadState.uploading),
reuploadRequestedBy: Value(reuploadRequestedBy), reuploadRequestedBy: Value(reuploadRequestedBy),
), ),
); );

View file

@ -57,13 +57,7 @@ ClientToServer createClientToServerFromApplicationData(
return ClientToServer()..v0 = v0; return ClientToServer()..v0 = v0;
} }
Future<void> deleteContact(int contactId) async { Future<void> rejectAndDeleteContact(int contactId) async {
await twonlyDB.signalDao.deleteAllByContactId(contactId);
await deleteSessionWithTarget(contactId);
await twonlyDB.contactsDao.deleteContactByUserId(contactId);
}
Future<void> rejectUser(int contactId) async {
await sendCipherText( await sendCipherText(
contactId, contactId,
EncryptedContent( EncryptedContent(
@ -72,6 +66,9 @@ Future<void> rejectUser(int contactId) async {
), ),
), ),
); );
await twonlyDB.signalDao.deleteAllByContactId(contactId);
await deleteSessionWithTarget(contactId);
await twonlyDB.contactsDao.deleteContactByUserId(contactId);
} }
Future<void> handleMediaError(MediaFile media) async { Future<void> handleMediaError(MediaFile media) async {

View file

@ -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<void> 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<void> 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);
}
}

View file

@ -1,12 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/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'; import 'package:twonly/src/utils/log.dart';
class MediaFileService { class MediaFileService {
@ -39,6 +39,28 @@ class MediaFileService {
} }
} }
Future<void> setDisplayLimit(int? displayLimitInMilliseconds) async {
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(
displayLimitInMilliseconds: Value(displayLimitInMilliseconds),
),
);
await updateFromDB();
}
Future<void> 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<void> createThumbnail() async { Future<void> createThumbnail() async {
if (!storedPath.existsSync()) { if (!storedPath.existsSync()) {
Log.error('Could not create Thumbnail as stored media does not exists.'); Log.error('Could not create Thumbnail as stored media does not exists.');
@ -54,18 +76,35 @@ class MediaFileService {
} }
} }
Future<void> 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() { void fullMediaRemoval() {
if (tempPath.existsSync()) { final pathsToRemove = [
tempPath.deleteSync(); tempPath,
} encryptedPath,
if (encryptedPath.existsSync()) { originalPath,
encryptedPath.deleteSync(); storedPath,
} thumbnailPath
if (storedPath.existsSync()) { ];
storedPath.deleteSync();
} for (final path in pathsToRemove) {
if (thumbnailPath.existsSync()) { if (path.existsSync()) {
thumbnailPath.deleteSync(); path.deleteSync();
}
} }
} }
@ -121,4 +160,8 @@ class MediaFileService {
'tmp', 'tmp',
namePrefix: '.encrypted', namePrefix: '.encrypted',
); );
File get originalPath => _buildFilePath(
'tmp',
namePrefix: '.original',
);
} }

View file

@ -4,7 +4,7 @@ import 'package:drift/drift.dart';
import 'package:hashlib/hashlib.dart'; import 'package:hashlib/hashlib.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/services/api/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/services/twonly_safe/create_backup.twonly_safe.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';

View file

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

View file

@ -1,6 +1,4 @@
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -11,14 +9,13 @@ import 'package:local_auth/local_auth.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/localization/generated/app_localizations.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/model/protobuf/api/websocket/error.pb.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
extension ShortCutsExtension on BuildContext { extension ShortCutsExtension on BuildContext {
AppLocalizations get lang => AppLocalizations.of(this)!; AppLocalizations get lang => AppLocalizations.of(this)!;
TwonlyDatabase get db => Provider.of<TwonlyDatabase>(this); TwonlyDB get db => Provider.of<TwonlyDB>(this);
ColorScheme get color => Theme.of(this).colorScheme; ColorScheme get color => Theme.of(this).colorScheme;
} }
@ -245,31 +242,19 @@ String formatBytes(int bytes, {int decimalPlaces = 2}) {
return '${formattedSize.toStringAsFixed(decimalPlaces)} ${units[unitIndex]}'; 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) { bool isUUIDNewer(String uuid1, String uuid2) {
final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16); final timestamp1 = int.parse(uuid1.substring(0, 8), radix: 16);
final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16); final timestamp2 = int.parse(uuid2.substring(0, 8), radix: 16);
return timestamp1 > timestamp2; return timestamp1 > timestamp2;
} }
String uint8ListToHex(List<int> bytes) {
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
}
Uint8List hexToUint8List(String hex) => Uint8List.fromList(
List<int>.generate(
hex.length ~/ 2,
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16),
),
);

View file

@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/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/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
@ -14,6 +15,7 @@ Future<bool> isUserCreated() async {
if (user == null) { if (user == null) {
return false; return false;
} }
gUser = user;
return true; return true;
} }
@ -56,7 +58,8 @@ Future<UserData?> updateUserdata(
final updated = updateUser(user); final updated = updateUser(user);
await const FlutterSecureStorage() await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); .write(key: SecureStorageKeys.userData, value: jsonEncode(updated));
return user; gUser = updated;
return updated;
}); });
} }

View file

@ -7,8 +7,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:path/path.dart'; import 'package:path/path.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/thumbnail.service.dart'; import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';

View file

@ -10,7 +10,9 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -299,17 +301,35 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
File? videoFilePath, { File? videoFilePath, {
bool sharedFromGallery = false, bool sharedFromGallery = false,
}) async { }) 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( final shouldReturn = await Navigator.push(
context, context,
PageRouteBuilder( PageRouteBuilder(
opaque: false, opaque: false,
pageBuilder: (context, a1, a2) => ShareImageEditorView( pageBuilder: (context, a1, a2) => ShareImageEditorView(
videoFilePath: videoFilePath, imageBytesFuture: imageBytes,
imageBytes: imageBytes,
sharedFromGallery: sharedFromGallery, sharedFromGallery: sharedFromGallery,
sendTo: widget.sendTo, sendTo: widget.sendTo,
mirrorVideo: isFront && Platform.isAndroid && false, mediaFileService: mediaFileService,
useHighQuality: true,
), ),
transitionsBuilder: (context, animation, secondaryAnimation, child) { transitionsBuilder: (context, animation, secondaryAnimation, child) {
return child; return child;

View file

@ -3,14 +3,19 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hashlib/random.dart';
import 'package:screenshot/screenshot.dart'; import 'package:screenshot/screenshot.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/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/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -34,32 +39,26 @@ const gMediaShowInfinite = 999999;
class ShareImageEditorView extends StatefulWidget { class ShareImageEditorView extends StatefulWidget {
const ShareImageEditorView({ const ShareImageEditorView({
required this.mirrorVideo,
required this.useHighQuality,
required this.sharedFromGallery, required this.sharedFromGallery,
required this.mediaFileService,
super.key, super.key,
this.imageBytes, this.imageBytesFuture,
this.sendTo, this.sendTo,
this.videoFilePath,
}); });
final Future<Uint8List?>? imageBytes; final Future<Uint8List?>? imageBytesFuture;
final File? videoFilePath; final Group? sendTo;
final Contact? sendTo;
final bool mirrorVideo;
final bool useHighQuality;
final bool sharedFromGallery; final bool sharedFromGallery;
final MediaFileService mediaFileService;
@override @override
State<ShareImageEditorView> createState() => _ShareImageEditorView(); State<ShareImageEditorView> createState() => _ShareImageEditorView();
} }
class _ShareImageEditorView extends State<ShareImageEditorView> { class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _isRealTwonly = false;
int maxShowTime = gMediaShowInfinite;
double tabDownPosition = 0; double tabDownPosition = 0;
bool sendingOrLoadingImage = true; bool sendingOrLoadingImage = true;
bool loadingImage = true; bool loadingImage = true;
bool isDisposed = false; bool isDisposed = false;
HashSet<int> selectedUserIds = HashSet(); HashSet<String> selectedGroupIds = HashSet();
double widthRatio = 1; double widthRatio = 1;
double heightRatio = 1; double heightRatio = 1;
double pixelRatio = 1; double pixelRatio = 1;
@ -68,26 +67,31 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
ScreenshotController screenshotController = ScreenshotController(); ScreenshotController screenshotController = ScreenshotController();
/// Media upload variables /// Media upload variables
int? mediaUploadId;
Future<bool>? videoUploadHandler; Future<bool>? videoUploadHandler;
MediaFileService get mediaService => widget.mediaFileService;
MediaFile get media => widget.mediaFileService.mediaFile;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
unawaited(initAsync());
unawaited(initMediaFileUpload());
layers.add(FilterLayerData()); layers.add(FilterLayerData());
if (widget.sendTo != null) { if (widget.sendTo != null) {
selectedUserIds.add(widget.sendTo!.userId); selectedGroupIds.add(widget.sendTo!.groupId);
} }
if (widget.imageBytes != null) {
unawaited(loadImage(widget.imageBytes!)); if (widget.imageBytesFuture != null) {
} else if (widget.videoFilePath != null) { unawaited(loadImage(widget.imageBytesFuture!));
}
if (media.type == MediaType.video) {
setState(() { setState(() {
sendingOrLoadingImage = false; sendingOrLoadingImage = false;
loadingImage = false; loadingImage = false;
}); });
videoController = VideoPlayerController.file(widget.videoFilePath!); videoController = VideoPlayerController.file(mediaService.originalPath);
videoController?.setLooping(true); videoController?.setLooping(true);
videoController?.initialize().then((_) async { videoController?.initialize().then((_) async {
await videoController!.play(); await videoController!.play();
@ -97,29 +101,6 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
} }
Future<void> initAsync() async {
final user = await getUser();
if (user == null) return;
if (user.defaultShowTime != null) {
setState(() {
maxShowTime = user.defaultShowTime!;
});
}
}
Future<void> 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 @override
void dispose() { void dispose() {
isDisposed = true; isDisposed = true;
@ -128,14 +109,14 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
super.dispose(); super.dispose();
} }
void updateStatus(int userId, bool checked) { void updateSelectedGroupIds(String groupId, bool checked) {
if (checked) { if (checked) {
if (_isRealTwonly) { if (media.requiresAuthentication) {
selectedUserIds.clear(); selectedGroupIds.clear();
} }
selectedUserIds.add(userId); selectedGroupIds.add(groupId);
} else { } else {
selectedUserIds.remove(userId); selectedGroupIds.remove(groupId);
} }
setState(() {}); setState(() {});
} }
@ -195,38 +176,36 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
NotificationBadge( NotificationBadge(
count: (widget.videoFilePath != null) count: (media.type != MediaType.video)
? '0' ? '0'
: maxShowTime == gMediaShowInfinite : media.displayLimitInMilliseconds == null
? '' ? ''
: maxShowTime.toString(), : media.displayLimitInMilliseconds.toString(),
child: ActionButton( child: ActionButton(
(widget.videoFilePath != null) (media.type != MediaType.video)
? maxShowTime == gMediaShowInfinite ? media.displayLimitInMilliseconds == null
? Icons.repeat_rounded ? Icons.repeat_rounded
: Icons.repeat_one_rounded : Icons.repeat_one_rounded
: Icons.timer_outlined, : Icons.timer_outlined,
tooltipText: context.lang.protectAsARealTwonly, tooltipText: context.lang.protectAsARealTwonly,
onPressed: () async { onPressed: () async {
if (widget.videoFilePath != null) { if (media.type != MediaType.video) {
setState(() { await mediaService.setDisplayLimit(
if (maxShowTime == gMediaShowInfinite) { (media.displayLimitInMilliseconds == null) ? 0 : null);
maxShowTime = 0; if (!mounted) return;
} else { setState(() {});
maxShowTime = gMediaShowInfinite;
}
});
return; return;
} }
if (maxShowTime == gMediaShowInfinite) { int? maxShowTime;
if (media.displayLimitInMilliseconds == null) {
maxShowTime = 1; maxShowTime = 1;
} else if (maxShowTime == 1) { } else if (media.displayLimitInMilliseconds == 1) {
maxShowTime = 5; maxShowTime = 5;
} else if (maxShowTime == 5) { } else if (media.displayLimitInMilliseconds == 5) {
maxShowTime = 20; maxShowTime = 20;
} else {
maxShowTime = gMediaShowInfinite;
} }
await mediaService.setDisplayLimit(maxShowTime);
if (!mounted) return;
setState(() {}); setState(() {});
await updateUserdata((user) { await updateUserdata((user) {
user.defaultShowTime = maxShowTime; user.defaultShowTime = maxShowTime;
@ -239,15 +218,12 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
ActionButton( ActionButton(
FontAwesomeIcons.shieldHeart, FontAwesomeIcons.shieldHeart,
tooltipText: context.lang.protectAsARealTwonly, tooltipText: context.lang.protectAsARealTwonly,
color: _isRealTwonly color: media.requiresAuthentication
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Colors.white, : Colors.white,
onPressed: () async { onPressed: () async {
_isRealTwonly = !_isRealTwonly; await mediaService.setRequiresAuth(!media.requiresAuthentication);
if (_isRealTwonly) { selectedGroupIds = HashSet();
maxShowTime = 12;
}
selectedUserIds = HashSet();
setState(() {}); setState(() {});
}, },
), ),
@ -308,11 +284,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
Future<void> pushShareImageView() async { Future<void> pushShareImageView() async {
if (mediaUploadId == null) { final imageBytes = storeImageAsOriginal();
await initMediaFileUpload();
if (mediaUploadId == null) return;
}
final imageBytes = getMergedImage();
await videoController?.pause(); await videoController?.pause();
if (isDisposed || !mounted) return; if (isDisposed || !mounted) return;
final wasSend = await Navigator.push( final wasSend = await Navigator.push(
@ -320,13 +292,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
imageBytesFuture: imageBytes, imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly, selectedUserIds: selectedGroupIds,
maxShowTime: maxShowTime, updateStatus: updateSelectedGroupIds,
selectedUserIds: selectedUserIds,
updateStatus: updateStatus,
videoUploadHandler: videoUploadHandler, videoUploadHandler: videoUploadHandler,
mediaUploadId: mediaUploadId!, mediaFileService: mediaService,
mirrorVideo: widget.mirrorVideo,
), ),
), ),
) as bool?; ) as bool?;
@ -337,36 +306,46 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
} }
} }
Future<Uint8List?> getMergedImage() async { Future<void> storeImageAsOriginal() async {
Uint8List? image; if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
final image = (layers.first as BackgroundLayerData).image.bytes;
mediaService.originalPath.writeAsBytesSync(image);
}
}
if (layers.length > 1 || media.type != MediaType.video) {
TODO: When changed then create a new mediaID!!!!!!
As storedMediaId would overwrite it....
if (layers.length > 1 || widget.videoFilePath != null) {
for (final x in layers) { for (final x in layers) {
x.showCustomButtons = false; x.showCustomButtons = false;
} }
setState(() {}); setState(() {});
image = await screenshotController.capture( final image = await screenshotController.capture(
pixelRatio: (widget.useHighQuality) ? pixelRatio : 1, 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) { for (final x in layers) {
x.showCustomButtons = true; x.showCustomButtons = true;
} }
setState(() {}); setState(() {});
} else if (layers.length == 1) {
if (layers.first is BackgroundLayerData) {
image = (layers.first as BackgroundLayerData).image.bytes;
}
} }
return image;
} }
Future<void> loadImage(Future<Uint8List?> imageFile) async { Future<void> loadImage(Future<Uint8List?> imageBytesFuture) async {
final imageBytes = await imageFile; await currentImage.load(await imageBytesFuture);
await currentImage.load(imageBytes);
if (isDisposed) return; if (isDisposed) return;
if (!context.mounted) return; if (!context.mounted) return;
@ -388,56 +367,29 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
setState(() { setState(() {
sendingOrLoadingImage = true; 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 (!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 // ignore: use_build_context_synchronously
Navigator.pop(context, true); 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<ShareImageEditorView> {
children: [ children: [
if (videoController != null) if (videoController != null)
Positioned.fill( Positioned.fill(
child: Transform.flip( child: VideoPlayer(videoController!),
flipX: widget.mirrorVideo,
child: VideoPlayer(videoController!),
),
), ),
Screenshot( Screenshot(
controller: screenshotController, controller: screenshotController,

View file

@ -9,7 +9,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
import 'package:twonly/src/database/twonly.db.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/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart'; import 'package:twonly/src/views/camera/share_image_components/best_friends_selector.dart';
import 'package:twonly/src/views/components/flame.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 { class ShareImageView extends StatefulWidget {
const ShareImageView({ const ShareImageView({
required this.imageBytesFuture, required this.imageBytesFuture,
required this.isRealTwonly,
required this.mirrorVideo,
required this.maxShowTime,
required this.selectedUserIds, required this.selectedUserIds,
required this.updateStatus, required this.updateStatus,
required this.videoUploadHandler, required this.videoUploadHandler,
required this.mediaUploadId, required this.mediaFileService,
super.key, super.key,
this.enableVideoAudio, this.enableVideoAudio,
}); });
final Future<Uint8List?> imageBytesFuture; final Future<Uint8List?> imageBytesFuture;
final bool isRealTwonly;
final bool mirrorVideo;
final int maxShowTime;
final HashSet<int> selectedUserIds; final HashSet<int> selectedUserIds;
final bool? enableVideoAudio; final bool? enableVideoAudio;
final int mediaUploadId;
final void Function(int, bool) updateStatus; final void Function(int, bool) updateStatus;
final Future<bool>? videoUploadHandler; final Future<bool>? videoUploadHandler;
final MediaFileService mediaFileService;
@override @override
State<ShareImageView> createState() => _ShareImageView(); State<ShareImageView> createState() => _ShareImageView();

View file

@ -227,8 +227,7 @@ class ContactsListView extends StatelessWidget {
child: IconButton( child: IconButton(
icon: const Icon(Icons.close, color: Colors.red), icon: const Icon(Icons.close, color: Colors.red),
onPressed: () async { onPressed: () async {
await rejectUser(contact.userId); await rejectAndDeleteContact(contact.userId);
await deleteContact(contact.userId);
}, },
), ),
), ),

View file

@ -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/database/twonly.db.dart';
import 'package:twonly/src/model/json/userdata.dart'; import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/providers/connection.provider.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera/camera_send_to_view.dart'; import 'package:twonly/src/views/camera/camera_send_to_view.dart';

View file

@ -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/json/message_old.dart';
import 'package:twonly/src/model/memory_item.model.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/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/services/api/messages.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';

View file

@ -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/database/twonly.db.dart';
import 'package:twonly/src/model/json/message_old.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/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/messages.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/notifications/background.notifications.dart'; import 'package:twonly/src/services/notifications/background.notifications.dart';

View file

@ -31,8 +31,7 @@ class _ContactViewState extends State<ContactView> {
); );
if (remove) { if (remove) {
// trigger deletion for the other user... // trigger deletion for the other user...
await rejectUser(contact.userId); await rejectAndDeleteContact(contact.userId);
await deleteContact(contact.userId);
if (mounted) { if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
} }

View file

@ -6,8 +6,8 @@ import 'package:intl/intl.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.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/api/mediafiles/upload.service.dart' as send;
import 'package:twonly/src/services/thumbnail.service.dart'; import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/views/memories/memories_item_thumbnail.dart'; import 'package:twonly/src/views/memories/memories_item_thumbnail.dart';
import 'package:twonly/src/views/memories/memories_photo_slider.view.dart'; import 'package:twonly/src/views/memories/memories_photo_slider.view.dart';

View file

@ -6,8 +6,9 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/memory_item.model.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/mediafiles/download.service.dart'
import 'package:twonly/src/services/api/media_upload.dart' as send; 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/utils/misc.dart';
import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/camera/share_image_editor_view.dart';
import 'package:twonly/src/views/components/alert_dialog.dart'; import 'package:twonly/src/views/components/alert_dialog.dart';

View file

@ -90,6 +90,8 @@ class _RegisterViewState extends State<RegisterView> {
await const FlutterSecureStorage() await const FlutterSecureStorage()
.write(key: SecureStorageKeys.userData, value: jsonEncode(userData)); .write(key: SecureStorageKeys.userData, value: jsonEncode(userData));
gUser = userData;
await apiService.authenticate(); await apiService.authenticate();
widget.callbackOnSuccess(); widget.callbackOnSuccess();
} }

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';

View file

@ -8,7 +8,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/constants/secure_storage_keys.dart'; import 'package:twonly/src/constants/secure_storage_keys.dart';
import 'package:twonly/src/model/protobuf/api/http/http_requests.pb.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; show createDownloadToken, uint8ListToHex;
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';

View file

@ -4,7 +4,7 @@ import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hashlib/random.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/misc.dart';
import 'package:twonly/src/utils/pow.dart'; import 'package:twonly/src/utils/pow.dart';
import 'package:twonly/src/views/components/animate_icon.dart'; import 'package:twonly/src/views/components/animate_icon.dart';