From 80d6f85350ac568b26201d883d7ef586af809c91 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Nov 2025 12:26:17 +0100 Subject: [PATCH] draft images, multiple bug fixes --- lib/app.dart | 9 ++- lib/src/database/daos/mediafiles.dao.dart | 21 ++++++- lib/src/database/tables/mediafiles.table.dart | 2 + lib/src/database/twonly.db.g.dart | 60 +++++++++++++++++++ lib/src/localization/app_de.arb | 3 +- lib/src/localization/app_en.arb | 3 +- .../generated/app_localizations.dart | 6 ++ .../generated/app_localizations_de.dart | 4 ++ .../generated/app_localizations_en.dart | 4 ++ lib/src/services/api.service.dart | 15 +++-- .../api/mediafiles/upload.service.dart | 31 +++++++++- .../mediafiles/compression.service.dart | 8 ++- .../mediafiles/mediafile.service.dart | 9 +++ .../background.notifications.dart | 2 +- .../camera_preview_controller_view.dart | 2 + .../views/camera/share_image_editor_view.dart | 13 ++++ lib/src/views/chats/media_viewer.view.dart | 4 +- lib/src/views/components/flame.dart | 6 ++ .../components/max_flame_list_title.dart | 9 +-- lib/src/views/home.view.dart | 18 ++++++ lib/src/views/onboarding/register.view.dart | 60 +++++++++++++++++-- 21 files changed, 261 insertions(+), 28 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 52371c3..74fd9b2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -156,7 +156,7 @@ class _AppMainWidgetState extends State { bool _showOnboarding = true; bool _isLoaded = false; - Future? _proofOfWork; + (Future?, bool) _proofOfWork = (null, false); @override void initState() { @@ -176,11 +176,14 @@ class _AppMainWidgetState extends State { if (!_isUserCreated && !_showDatabaseMigration) { // This means the user is in the onboarding screen, so start with the Proof of Work. - final proof = await apiService.getProofOfWork(); + final (proof, disabled) = await apiService.getProofOfWork(); if (proof != null) { Log.info('Starting with proof of work calculation.'); // Starting with the proof of work. - _proofOfWork = calculatePoW(proof.prefix, proof.difficulty.toInt()); + _proofOfWork = + (calculatePoW(proof.prefix, proof.difficulty.toInt()), false); + } else { + _proofOfWork = (null, disabled); } } diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 03fae40..b1e09d9 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -50,11 +50,27 @@ class MediaFilesDao extends DatabaseAccessor .write(updates); } + Future updateAllMediaFiles( + MediaFilesCompanion updates, + ) async { + await update(mediaFiles).write(updates); + } + Future getMediaFileById(String mediaId) async { return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) .getSingleOrNull(); } + Future getDraftMediaFile() async { + final medias = await (select(mediaFiles) + ..where((t) => t.isDraftMedia.equals(true))) + .get(); + if (medias.isEmpty) { + return null; + } + return medias.first; + } + Stream watchMedia(String mediaId) { return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId))) .watchSingleOrNull(); @@ -87,10 +103,9 @@ class MediaFilesDao extends DatabaseAccessor Future> getAllMediaFilesPendingUpload() async { return (select(mediaFiles) ..where( - (t) => - t.uploadState.equals(UploadState.initialized.name) | + (t) => (t.uploadState.equals(UploadState.initialized.name) | t.uploadState.equals(UploadState.uploadLimitReached.name) | - t.uploadState.equals(UploadState.preprocessing.name), + t.uploadState.equals(UploadState.preprocessing.name)), )) .get(); } diff --git a/lib/src/database/tables/mediafiles.table.dart b/lib/src/database/tables/mediafiles.table.dart index 6651485..af129d8 100644 --- a/lib/src/database/tables/mediafiles.table.dart +++ b/lib/src/database/tables/mediafiles.table.dart @@ -44,10 +44,12 @@ class MediaFiles extends Table { BoolColumn get requiresAuthentication => boolean().withDefault(const Constant(false))(); + BoolColumn get reopenByContact => boolean().withDefault(const Constant(false))(); BoolColumn get stored => boolean().withDefault(const Constant(false))(); + BoolColumn get isDraftMedia => boolean().withDefault(const Constant(false))(); TextColumn get reuploadRequestedBy => text().map(IntListTypeConverter()).nullable()(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index f3e9a93..0e7f60b 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -1905,6 +1905,16 @@ class $MediaFilesTable extends MediaFiles defaultConstraints: GeneratedColumn.constraintIsAlways('CHECK ("stored" IN (0, 1))'), defaultValue: const Constant(false)); + static const VerificationMeta _isDraftMediaMeta = + const VerificationMeta('isDraftMedia'); + @override + late final GeneratedColumn isDraftMedia = GeneratedColumn( + 'is_draft_media', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_draft_media" IN (0, 1))'), + defaultValue: const Constant(false)); @override late final GeneratedColumnWithTypeConverter?, String> reuploadRequestedBy = GeneratedColumn( @@ -1968,6 +1978,7 @@ class $MediaFilesTable extends MediaFiles requiresAuthentication, reopenByContact, stored, + isDraftMedia, reuploadRequestedBy, displayLimitInMilliseconds, removeAudio, @@ -2009,6 +2020,12 @@ class $MediaFilesTable extends MediaFiles context.handle(_storedMeta, stored.isAcceptableOrUnknown(data['stored']!, _storedMeta)); } + if (data.containsKey('is_draft_media')) { + context.handle( + _isDraftMediaMeta, + isDraftMedia.isAcceptableOrUnknown( + data['is_draft_media']!, _isDraftMediaMeta)); + } if (data.containsKey('display_limit_in_milliseconds')) { context.handle( _displayLimitInMillisecondsMeta, @@ -2076,6 +2093,8 @@ class $MediaFilesTable extends MediaFiles DriftSqlType.bool, data['${effectivePrefix}reopen_by_contact'])!, stored: attachedDatabase.typeMapping .read(DriftSqlType.bool, data['${effectivePrefix}stored'])!, + isDraftMedia: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}is_draft_media'])!, reuploadRequestedBy: $MediaFilesTable.$converterreuploadRequestedByn .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}reupload_requested_by'])), @@ -2129,6 +2148,7 @@ class MediaFile extends DataClass implements Insertable { final bool requiresAuthentication; final bool reopenByContact; final bool stored; + final bool isDraftMedia; final List? reuploadRequestedBy; final int? displayLimitInMilliseconds; final bool? removeAudio; @@ -2145,6 +2165,7 @@ class MediaFile extends DataClass implements Insertable { required this.requiresAuthentication, required this.reopenByContact, required this.stored, + required this.isDraftMedia, this.reuploadRequestedBy, this.displayLimitInMilliseconds, this.removeAudio, @@ -2172,6 +2193,7 @@ class MediaFile extends DataClass implements Insertable { map['requires_authentication'] = Variable(requiresAuthentication); map['reopen_by_contact'] = Variable(reopenByContact); map['stored'] = Variable(stored); + map['is_draft_media'] = Variable(isDraftMedia); if (!nullToAbsent || reuploadRequestedBy != null) { map['reupload_requested_by'] = Variable($MediaFilesTable .$converterreuploadRequestedByn @@ -2213,6 +2235,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication: Value(requiresAuthentication), reopenByContact: Value(reopenByContact), stored: Value(stored), + isDraftMedia: Value(isDraftMedia), reuploadRequestedBy: reuploadRequestedBy == null && nullToAbsent ? const Value.absent() : Value(reuploadRequestedBy), @@ -2254,6 +2277,7 @@ class MediaFile extends DataClass implements Insertable { serializer.fromJson(json['requiresAuthentication']), reopenByContact: serializer.fromJson(json['reopenByContact']), stored: serializer.fromJson(json['stored']), + isDraftMedia: serializer.fromJson(json['isDraftMedia']), reuploadRequestedBy: serializer.fromJson?>(json['reuploadRequestedBy']), displayLimitInMilliseconds: @@ -2280,6 +2304,7 @@ class MediaFile extends DataClass implements Insertable { 'requiresAuthentication': serializer.toJson(requiresAuthentication), 'reopenByContact': serializer.toJson(reopenByContact), 'stored': serializer.toJson(stored), + 'isDraftMedia': serializer.toJson(isDraftMedia), 'reuploadRequestedBy': serializer.toJson?>(reuploadRequestedBy), 'displayLimitInMilliseconds': serializer.toJson(displayLimitInMilliseconds), @@ -2300,6 +2325,7 @@ class MediaFile extends DataClass implements Insertable { bool? requiresAuthentication, bool? reopenByContact, bool? stored, + bool? isDraftMedia, Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value removeAudio = const Value.absent(), @@ -2318,6 +2344,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, reuploadRequestedBy: reuploadRequestedBy.present ? reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -2352,6 +2379,9 @@ class MediaFile extends DataClass implements Insertable { ? data.reopenByContact.value : this.reopenByContact, stored: data.stored.present ? data.stored.value : this.stored, + isDraftMedia: data.isDraftMedia.present + ? data.isDraftMedia.value + : this.isDraftMedia, reuploadRequestedBy: data.reuploadRequestedBy.present ? data.reuploadRequestedBy.value : this.reuploadRequestedBy, @@ -2386,6 +2416,7 @@ class MediaFile extends DataClass implements Insertable { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('removeAudio: $removeAudio, ') @@ -2407,6 +2438,7 @@ class MediaFile extends DataClass implements Insertable { requiresAuthentication, reopenByContact, stored, + isDraftMedia, reuploadRequestedBy, displayLimitInMilliseconds, removeAudio, @@ -2426,6 +2458,7 @@ class MediaFile extends DataClass implements Insertable { other.requiresAuthentication == this.requiresAuthentication && other.reopenByContact == this.reopenByContact && other.stored == this.stored && + other.isDraftMedia == this.isDraftMedia && other.reuploadRequestedBy == this.reuploadRequestedBy && other.displayLimitInMilliseconds == this.displayLimitInMilliseconds && other.removeAudio == this.removeAudio && @@ -2445,6 +2478,7 @@ class MediaFilesCompanion extends UpdateCompanion { final Value requiresAuthentication; final Value reopenByContact; final Value stored; + final Value isDraftMedia; final Value?> reuploadRequestedBy; final Value displayLimitInMilliseconds; final Value removeAudio; @@ -2462,6 +2496,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.removeAudio = const Value.absent(), @@ -2480,6 +2515,7 @@ class MediaFilesCompanion extends UpdateCompanion { this.requiresAuthentication = const Value.absent(), this.reopenByContact = const Value.absent(), this.stored = const Value.absent(), + this.isDraftMedia = const Value.absent(), this.reuploadRequestedBy = const Value.absent(), this.displayLimitInMilliseconds = const Value.absent(), this.removeAudio = const Value.absent(), @@ -2499,6 +2535,7 @@ class MediaFilesCompanion extends UpdateCompanion { Expression? requiresAuthentication, Expression? reopenByContact, Expression? stored, + Expression? isDraftMedia, Expression? reuploadRequestedBy, Expression? displayLimitInMilliseconds, Expression? removeAudio, @@ -2518,6 +2555,7 @@ class MediaFilesCompanion extends UpdateCompanion { 'requires_authentication': requiresAuthentication, if (reopenByContact != null) 'reopen_by_contact': reopenByContact, if (stored != null) 'stored': stored, + if (isDraftMedia != null) 'is_draft_media': isDraftMedia, if (reuploadRequestedBy != null) 'reupload_requested_by': reuploadRequestedBy, if (displayLimitInMilliseconds != null) @@ -2540,6 +2578,7 @@ class MediaFilesCompanion extends UpdateCompanion { Value? requiresAuthentication, Value? reopenByContact, Value? stored, + Value? isDraftMedia, Value?>? reuploadRequestedBy, Value? displayLimitInMilliseconds, Value? removeAudio, @@ -2558,6 +2597,7 @@ class MediaFilesCompanion extends UpdateCompanion { requiresAuthentication ?? this.requiresAuthentication, reopenByContact: reopenByContact ?? this.reopenByContact, stored: stored ?? this.stored, + isDraftMedia: isDraftMedia ?? this.isDraftMedia, reuploadRequestedBy: reuploadRequestedBy ?? this.reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds ?? this.displayLimitInMilliseconds, @@ -2599,6 +2639,9 @@ class MediaFilesCompanion extends UpdateCompanion { if (stored.present) { map['stored'] = Variable(stored.value); } + if (isDraftMedia.present) { + map['is_draft_media'] = Variable(isDraftMedia.value); + } if (reuploadRequestedBy.present) { map['reupload_requested_by'] = Variable($MediaFilesTable .$converterreuploadRequestedByn @@ -2642,6 +2685,7 @@ class MediaFilesCompanion extends UpdateCompanion { ..write('requiresAuthentication: $requiresAuthentication, ') ..write('reopenByContact: $reopenByContact, ') ..write('stored: $stored, ') + ..write('isDraftMedia: $isDraftMedia, ') ..write('reuploadRequestedBy: $reuploadRequestedBy, ') ..write('displayLimitInMilliseconds: $displayLimitInMilliseconds, ') ..write('removeAudio: $removeAudio, ') @@ -9161,6 +9205,7 @@ typedef $$MediaFilesTableCreateCompanionBuilder = MediaFilesCompanion Function({ Value requiresAuthentication, Value reopenByContact, Value stored, + Value isDraftMedia, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value removeAudio, @@ -9179,6 +9224,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({ Value requiresAuthentication, Value reopenByContact, Value stored, + Value isDraftMedia, Value?> reuploadRequestedBy, Value displayLimitInMilliseconds, Value removeAudio, @@ -9248,6 +9294,9 @@ class $$MediaFilesTableFilterComposer ColumnFilters get stored => $composableBuilder( column: $table.stored, builder: (column) => ColumnFilters(column)); + ColumnFilters get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters?, List, String> get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, @@ -9331,6 +9380,10 @@ class $$MediaFilesTableOrderingComposer ColumnOrderings get stored => $composableBuilder( column: $table.stored, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, builder: (column) => ColumnOrderings(column)); @@ -9394,6 +9447,9 @@ class $$MediaFilesTableAnnotationComposer GeneratedColumn get stored => $composableBuilder(column: $table.stored, builder: (column) => column); + GeneratedColumn get isDraftMedia => $composableBuilder( + column: $table.isDraftMedia, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get reuploadRequestedBy => $composableBuilder( column: $table.reuploadRequestedBy, builder: (column) => column); @@ -9471,6 +9527,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value stored = const Value.absent(), + Value isDraftMedia = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value removeAudio = const Value.absent(), @@ -9489,6 +9546,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, stored: stored, + isDraftMedia: isDraftMedia, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, removeAudio: removeAudio, @@ -9507,6 +9565,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< Value requiresAuthentication = const Value.absent(), Value reopenByContact = const Value.absent(), Value stored = const Value.absent(), + Value isDraftMedia = const Value.absent(), Value?> reuploadRequestedBy = const Value.absent(), Value displayLimitInMilliseconds = const Value.absent(), Value removeAudio = const Value.absent(), @@ -9525,6 +9584,7 @@ class $$MediaFilesTableTableManager extends RootTableManager< requiresAuthentication: requiresAuthentication, reopenByContact: reopenByContact, stored: stored, + isDraftMedia: isDraftMedia, reuploadRequestedBy: reuploadRequestedBy, displayLimitInMilliseconds: displayLimitInMilliseconds, removeAudio: removeAudio, diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 85299bc..5e7bba5 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -817,5 +817,6 @@ "deleteChatAfterAWeek": "einer Woche.", "deleteChatAfterAMonth": "einem Monat.", "deleteChatAfterAYear": "einem Jahr.", - "yourTwonlyScore": "Dein twonly-Score" + "yourTwonlyScore": "Dein twonly-Score", + "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 4078005..cb78dfd 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -595,5 +595,6 @@ "deleteChatAfterAWeek": "one week.", "deleteChatAfterAMonth": "one month.", "deleteChatAfterAYear": "one year.", - "yourTwonlyScore": "Your twonly-Score" + "yourTwonlyScore": "Your twonly-Score", + "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index db8ac2b..feedd96 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2677,6 +2677,12 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Your twonly-Score'** String get yourTwonlyScore; + + /// No description provided for @registrationClosed. + /// + /// In en, this message translates to: + /// **'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'** + String get registrationClosed; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index ebe795b..fe61244 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1477,4 +1477,8 @@ class AppLocalizationsDe extends AppLocalizations { @override String get yourTwonlyScore => 'Dein twonly-Score'; + + @override + String get registrationClosed => + 'Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 607bc3f..790bd19 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1467,4 +1467,8 @@ class AppLocalizationsEn extends AppLocalizations { @override String get yourTwonlyScore => 'Your twonly-Score'; + + @override + String get registrationClosed => + 'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'; } diff --git a/lib/src/services/api.service.dart b/lib/src/services/api.service.dart index 4cc3e23..86651df 100644 --- a/lib/src/services/api.service.dart +++ b/lib/src/services/api.service.dart @@ -51,8 +51,9 @@ final lockRetransStore = Mutex(); /// errors or network changes. class ApiService { ApiService(); - final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; - final String apiSecure = kReleaseMode ? 's' : ''; + // final String apiHost = kReleaseMode ? 'api.twonly.eu' : '10.99.0.140:3030'; + final String apiHost = kReleaseMode ? 'api.twonly.eu' : 'dev.twonly.eu'; + final String apiSecure = kReleaseMode ? 's' : 's'; bool appIsOutdated = false; bool isAuthenticated = false; @@ -508,15 +509,19 @@ class ApiService { return null; } - Future getProofOfWork() async { + Future<(Response_ProofOfWork?, bool)> getProofOfWork() async { final handshake = Handshake()..requestPOW = Handshake_RequestPOW(); final req = createClientToServerFromHandshake(handshake); final result = await sendRequestSync(req, authenticated: false); if (result.isError) { Log.error('could not request proof of work params', result); - return null; + if (result.error == ErrorCode.RegistrationDisabled) { + return (null, true); + } + Log.error('could not request proof of work params', result); + return (null, false); } - return result.value.proofOfWork as Response_ProofOfWork; + return (result.value.proofOfWork as Response_ProofOfWork, false); } Future downloadDone(List token) async { diff --git a/lib/src/services/api/mediafiles/upload.service.dart b/lib/src/services/api/mediafiles/upload.service.dart index d4a6783..7fa39d9 100644 --- a/lib/src/services/api/mediafiles/upload.service.dart +++ b/lib/src/services/api/mediafiles/upload.service.dart @@ -24,8 +24,24 @@ Future finishStartedPreprocessing() async { await twonlyDB.mediaFilesDao.getAllMediaFilesPendingUpload(); for (final mediaFile in mediaFiles) { + if (mediaFile.isDraftMedia) { + continue; + } try { final service = await MediaFileService.fromMedia(mediaFile); + if (!service.originalPath.existsSync() && + !service.uploadRequestPath.existsSync()) { + if (service.storedPath.existsSync()) { + // media files was just stored.. + continue; + } + Log.info( + 'Deleted media files, as originalPath and uploadRequestPath both do not exists', + ); + // the file does not exists anymore. + await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId); + continue; + } await startBackgroundMediaUpload(service); } catch (e) { Log.error(e); @@ -35,18 +51,24 @@ Future finishStartedPreprocessing() async { Future initializeMediaUpload( MediaType type, - int? displayLimitInMilliseconds, -) async { + int? displayLimitInMilliseconds, { + bool isDraftMedia = false, +}) async { final chacha20 = FlutterChacha20.poly1305Aead(); final encryptionKey = await (await chacha20.newSecretKey()).extract(); final encryptionNonce = chacha20.newNonce(); + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion(isDraftMedia: Value(false)), + ); + final mediaFile = await twonlyDB.mediaFilesDao.insertMedia( MediaFilesCompanion( uploadState: const Value(UploadState.initialized), displayLimitInMilliseconds: Value(displayLimitInMilliseconds), encryptionKey: Value(Uint8List.fromList(encryptionKey.bytes)), encryptionNonce: Value(Uint8List.fromList(encryptionNonce)), + isDraftMedia: Value(isDraftMedia), type: Value(type), ), ); @@ -58,6 +80,11 @@ Future insertMediaFileInMessagesTable( MediaFileService mediaService, List groupIds, ) async { + await twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), + ), + ); for (final groupId in groupIds) { final message = await twonlyDB.messagesDao.insertMessage( MessagesCompanion( diff --git a/lib/src/services/mediafiles/compression.service.dart b/lib/src/services/mediafiles/compression.service.dart index b09f33e..e1da17c 100644 --- a/lib/src/services/mediafiles/compression.service.dart +++ b/lib/src/services/mediafiles/compression.service.dart @@ -65,20 +65,24 @@ Future compressAndOverlayVideo(MediaFileService media) async { if (media.tempPath.existsSync()) { media.tempPath.deleteSync(); } + if (media.ffmpegOutputPath.existsSync()) { + media.ffmpegOutputPath.deleteSync(); + } final stopwatch = Stopwatch()..start(); var command = - '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.tempPath.path}"'; + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -map "0:a?" -preset veryfast -crf 28 -c:a aac -b:a 64k "${media.ffmpegOutputPath.path}"'; if (media.removeAudio) { command = - '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.tempPath.path}"'; + '-i "${media.originalPath.path}" -i "${media.overlayImagePath.path}" -filter_complex "[1:v][0:v]scale2ref=w=ref_w:h=ref_h[ovr][base];[base][ovr]overlay=0:0" -preset veryfast -crf 28 -an "${media.ffmpegOutputPath.path}"'; } final session = await FFmpegKit.execute(command); final returnCode = await session.getReturnCode(); if (ReturnCode.isSuccess(returnCode)) { + media.ffmpegOutputPath.copySync(media.tempPath.path); stopwatch.stop(); Log.info( 'It took ${stopwatch.elapsedMilliseconds}ms to compress the video', diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 27e2ea1..00bed5a 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -45,11 +45,16 @@ class MediaFileService { var delete = true; final service = await MediaFileService.fromMediaId(mediaId); + if (service == null) { Log.error( 'Purging media file, as it is not in the database $mediaId.', ); } else { + if (service.mediaFile.isDraftMedia) { + delete = false; + } + final messages = await twonlyDB.messagesDao.getMessagesByMediaId(mediaId); @@ -302,6 +307,10 @@ class MediaFileService { 'tmp', namePrefix: '.original', ); + File get ffmpegOutputPath => _buildFilePath( + 'tmp', + namePrefix: '.ffmpeg', + ); File get overlayImagePath => _buildFilePath( 'tmp', namePrefix: '.overlay', diff --git a/lib/src/services/notifications/background.notifications.dart b/lib/src/services/notifications/background.notifications.dart index 905c3ed..2d6613f 100644 --- a/lib/src/services/notifications/background.notifications.dart +++ b/lib/src/services/notifications/background.notifications.dart @@ -253,7 +253,7 @@ String getPushNotificationText(PushNotification pushNotification) { PushKind.twonly.name: lang.notificationTwonly(inGroup), PushKind.video.name: lang.notificationVideo(inGroup), PushKind.image.name: lang.notificationImage(inGroup), - PushKind.video.name: lang.notificationAudio(inGroup), + PushKind.audio.name: lang.notificationAudio(inGroup), PushKind.contactRequest.name: lang.notificationContactRequest, PushKind.acceptRequest.name: lang.notificationAcceptRequest, PushKind.storedMediaFile.name: lang.notificationStoredMediaFile, diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 5a4a93c..bab3b3b 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart'; @@ -352,6 +353,7 @@ class _CameraPreviewViewState extends State { final mediaFileService = await initializeMediaUpload( type, gUser.defaultShowTime, + isDraftMedia: true, ); if (!mounted) return true; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index bfa5c3b..3db605c 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'dart:collection'; +import 'package:drift/drift.dart' show Value; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hashlib/random.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -82,6 +84,8 @@ class _ShareImageEditorView extends State { } else { if (widget.mediaFileService.tempPath.existsSync()) { loadImage(widget.mediaFileService.tempPath.readAsBytes()); + } else if (widget.mediaFileService.originalPath.existsSync()) { + loadImage(widget.mediaFileService.originalPath.readAsBytes()); } } } @@ -106,6 +110,11 @@ class _ShareImageEditorView extends State { isDisposed = true; layers.clear(); videoController?.dispose(); + twonlyDB.mediaFilesDao.updateAllMediaFiles( + const MediaFilesCompanion( + isDraftMedia: Value(false), + ), + ); super.dispose(); } @@ -388,6 +397,10 @@ class _ShareImageEditorView extends State { Future loadImage(Future imageBytesFuture) async { imageBytes = await imageBytesFuture; + + // store this image so it can be used as a draft in case the app is restarted + mediaService.originalPath.writeAsBytesSync(imageBytes!.toList()); + await currentImage.load(imageBytes); if (isDisposed) return; diff --git a/lib/src/views/chats/media_viewer.view.dart b/lib/src/views/chats/media_viewer.view.dart index 9fe19f9..471cc42 100644 --- a/lib/src/views/chats/media_viewer.view.dart +++ b/lib/src/views/chats/media_viewer.view.dart @@ -476,8 +476,8 @@ class _MediaViewerViewState extends State { if (videoController != null) Positioned.fill( child: VideoPlayer(videoController!), - ), - if (currentMedia != null && + ) + else if (currentMedia != null && currentMedia!.mediaFile.type == MediaType.image || currentMedia!.mediaFile.type == MediaType.gif) Positioned.fill( diff --git a/lib/src/views/components/flame.dart b/lib/src/views/components/flame.dart index 122d014..dc83b12 100644 --- a/lib/src/views/components/flame.dart +++ b/lib/src/views/components/flame.dart @@ -41,6 +41,12 @@ class _FlameCounterWidgetState extends State { if (widget.groupId == null && widget.contactId != null) { final group = await twonlyDB.groupsDao.getDirectChat(widget.contactId!); groupId = group?.groupId; + } else if (groupId != null) { + // do not display the flame counter for groups + final group = await twonlyDB.groupsDao.getGroup(groupId); + if (!(group?.isDirectChat ?? false)) { + return; + } } if (groupId != null) { isBestFriend = gUser.myBestFriendGroupId == groupId; diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index efe80ba..ba490e8 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -36,7 +36,8 @@ class _MaxFlameListTitleState extends State { _flameCounterSub = stream.listen((counter) { if (mounted) { setState(() { - _flameCounter = counter; + _flameCounter = counter - + 1; // in the watchFlameCounter a one is added, so remove this here }); } }); @@ -73,7 +74,7 @@ class _MaxFlameListTitleState extends State { await twonlyDB.groupsDao.updateGroup( _groupId, GroupsCompanion( - flameCounter: Value(_directChat!.maxFlameCounter - 1), + flameCounter: Value(_directChat!.maxFlameCounter), lastFlameCounterChange: Value(DateTime.now()), ), ); @@ -84,7 +85,7 @@ class _MaxFlameListTitleState extends State { Widget build(BuildContext context) { if (_directChat == null || _directChat!.maxFlameCounter == 0 || - _flameCounter >= (_directChat!.maxFlameCounter + 1) || + _flameCounter >= _directChat!.maxFlameCounter || _directChat!.maxFlameCounterFrom! .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { return Container(); @@ -97,7 +98,7 @@ class _MaxFlameListTitleState extends State { emoji: '🔥', ), ), - text: 'Restore your ${_directChat!.maxFlameCounter} lost flames', + text: 'Restore your ${_directChat!.maxFlameCounter + 1} lost flames', ); } } diff --git a/lib/src/views/home.view.dart b/lib/src/views/home.view.dart index ee2907d..53ae88b 100644 --- a/lib/src/views/home.view.dart +++ b/lib/src/views/home.view.dart @@ -4,10 +4,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:screenshot/screenshot.dart'; +import 'package:twonly/globals.dart'; +import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/notifications/setup.notifications.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/views/camera/camera_preview_components/camera_preview.dart'; import 'package:twonly/src/views/camera/camera_preview_controller_view.dart'; +import 'package:twonly/src/views/camera/share_image_editor_view.dart'; import 'package:twonly/src/views/chats/chat_list.view.dart'; import 'package:twonly/src/views/memories/memories.view.dart'; @@ -145,6 +148,21 @@ class HomeViewState extends State { globalUpdateOfHomeViewPageIndex(0); } } + + final draftMedia = await twonlyDB.mediaFilesDao.getDraftMediaFile(); + if (draftMedia != null) { + final service = await MediaFileService.fromMedia(draftMedia); + if (!mounted) return; + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ShareImageEditorView( + mediaFileService: service, + sharedFromGallery: true, + ), + ), + ); + } } @override diff --git a/lib/src/views/onboarding/register.view.dart b/lib/src/views/onboarding/register.view.dart index d06df15..67b6f1c 100644 --- a/lib/src/views/onboarding/register.view.dart +++ b/lib/src/views/onboarding/register.view.dart @@ -27,7 +27,7 @@ class RegisterView extends StatefulWidget { }); final Function callbackOnSuccess; - final Future? proofOfWork; + final (Future?, bool) proofOfWork; @override State createState() => _RegisterViewState(); } @@ -36,10 +36,20 @@ class _RegisterViewState extends State { final TextEditingController usernameController = TextEditingController(); final TextEditingController inviteCodeController = TextEditingController(); + bool _registrationDisabled = false; bool _isTryingToRegister = false; bool _isValidUserName = false; bool _showUserNameError = false; + late Future? proofOfWork; + + @override + void initState() { + proofOfWork = widget.proofOfWork.$1; + _registrationDisabled = widget.proofOfWork.$2; + super.initState(); + } + Future createNewUser() async { if (!_isValidUserName) { setState(() { @@ -57,11 +67,12 @@ class _RegisterViewState extends State { late int proof; - if (widget.proofOfWork != null) { - proof = await widget.proofOfWork!; + if (proofOfWork != null) { + proof = await proofOfWork!; } else { - final pow = await apiService.getProofOfWork(); + final (pow, registrationDisabled) = await apiService.getProofOfWork(); if (pow == null) { + _registrationDisabled = registrationDisabled; if (mounted) { showNetworkIssue(context); } @@ -82,6 +93,10 @@ class _RegisterViewState extends State { Log.info('Got user_id ${res.value} from server'); userId = res.value.userid.toInt() as int; } else { + if (res.error == ErrorCode.RegistrationDisabled) { + _registrationDisabled = true; + return; + } if (res.error == ErrorCode.UserIdAlreadyTaken) { Log.error('User ID already token. Tying again.'); await deleteLocalUserData(); @@ -127,6 +142,43 @@ class _RegisterViewState extends State { @override Widget build(BuildContext context) { + if (_registrationDisabled) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(10), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ListView( + children: [ + const SizedBox(height: 50), + Text( + context.lang.registerTitle, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 30), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Text( + context.lang.registerSlogan, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 12), + ), + ), + const SizedBox(height: 130), + Text( + context.lang.registrationClosed, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.red, + ), + ), + ], + ), + ), + ), + ); + } + InputDecoration getInputDecoration(String hintText) { return InputDecoration(hintText: hintText, fillColor: Colors.grey[400]); }