draft images, multiple bug fixes

This commit is contained in:
otsmr 2025-11-08 12:26:17 +01:00
parent aebf6de4a5
commit 80d6f85350
21 changed files with 261 additions and 28 deletions

View file

@ -156,7 +156,7 @@ class _AppMainWidgetState extends State<AppMainWidget> {
bool _showOnboarding = true;
bool _isLoaded = false;
Future<int>? _proofOfWork;
(Future<int>?, bool) _proofOfWork = (null, false);
@override
void initState() {
@ -176,11 +176,14 @@ class _AppMainWidgetState extends State<AppMainWidget> {
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);
}
}

View file

@ -50,11 +50,27 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.write(updates);
}
Future<void> updateAllMediaFiles(
MediaFilesCompanion updates,
) async {
await update(mediaFiles).write(updates);
}
Future<MediaFile?> getMediaFileById(String mediaId) async {
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
.getSingleOrNull();
}
Future<MediaFile?> getDraftMediaFile() async {
final medias = await (select(mediaFiles)
..where((t) => t.isDraftMedia.equals(true)))
.get();
if (medias.isEmpty) {
return null;
}
return medias.first;
}
Stream<MediaFile?> watchMedia(String mediaId) {
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
.watchSingleOrNull();
@ -87,10 +103,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
Future<List<MediaFile>> 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();
}

View file

@ -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()();

View file

@ -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<bool> isDraftMedia = GeneratedColumn<bool>(
'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<List<int>?, String>
reuploadRequestedBy = GeneratedColumn<String>(
@ -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<MediaFile> {
final bool requiresAuthentication;
final bool reopenByContact;
final bool stored;
final bool isDraftMedia;
final List<int>? reuploadRequestedBy;
final int? displayLimitInMilliseconds;
final bool? removeAudio;
@ -2145,6 +2165,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
map['requires_authentication'] = Variable<bool>(requiresAuthentication);
map['reopen_by_contact'] = Variable<bool>(reopenByContact);
map['stored'] = Variable<bool>(stored);
map['is_draft_media'] = Variable<bool>(isDraftMedia);
if (!nullToAbsent || reuploadRequestedBy != null) {
map['reupload_requested_by'] = Variable<String>($MediaFilesTable
.$converterreuploadRequestedByn
@ -2213,6 +2235,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
serializer.fromJson<bool>(json['requiresAuthentication']),
reopenByContact: serializer.fromJson<bool>(json['reopenByContact']),
stored: serializer.fromJson<bool>(json['stored']),
isDraftMedia: serializer.fromJson<bool>(json['isDraftMedia']),
reuploadRequestedBy:
serializer.fromJson<List<int>?>(json['reuploadRequestedBy']),
displayLimitInMilliseconds:
@ -2280,6 +2304,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'requiresAuthentication': serializer.toJson<bool>(requiresAuthentication),
'reopenByContact': serializer.toJson<bool>(reopenByContact),
'stored': serializer.toJson<bool>(stored),
'isDraftMedia': serializer.toJson<bool>(isDraftMedia),
'reuploadRequestedBy': serializer.toJson<List<int>?>(reuploadRequestedBy),
'displayLimitInMilliseconds':
serializer.toJson<int?>(displayLimitInMilliseconds),
@ -2300,6 +2325,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
bool? requiresAuthentication,
bool? reopenByContact,
bool? stored,
bool? isDraftMedia,
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> removeAudio = const Value.absent(),
@ -2318,6 +2344,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
? 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<MediaFile> {
..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<MediaFile> {
requiresAuthentication,
reopenByContact,
stored,
isDraftMedia,
reuploadRequestedBy,
displayLimitInMilliseconds,
removeAudio,
@ -2426,6 +2458,7 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
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<MediaFile> {
final Value<bool> requiresAuthentication;
final Value<bool> reopenByContact;
final Value<bool> stored;
final Value<bool> isDraftMedia;
final Value<List<int>?> reuploadRequestedBy;
final Value<int?> displayLimitInMilliseconds;
final Value<bool?> removeAudio;
@ -2462,6 +2496,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
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<MediaFile> {
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<MediaFile> {
Expression<bool>? requiresAuthentication,
Expression<bool>? reopenByContact,
Expression<bool>? stored,
Expression<bool>? isDraftMedia,
Expression<String>? reuploadRequestedBy,
Expression<int>? displayLimitInMilliseconds,
Expression<bool>? removeAudio,
@ -2518,6 +2555,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
'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<MediaFile> {
Value<bool>? requiresAuthentication,
Value<bool>? reopenByContact,
Value<bool>? stored,
Value<bool>? isDraftMedia,
Value<List<int>?>? reuploadRequestedBy,
Value<int?>? displayLimitInMilliseconds,
Value<bool?>? removeAudio,
@ -2558,6 +2597,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
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<MediaFile> {
if (stored.present) {
map['stored'] = Variable<bool>(stored.value);
}
if (isDraftMedia.present) {
map['is_draft_media'] = Variable<bool>(isDraftMedia.value);
}
if (reuploadRequestedBy.present) {
map['reupload_requested_by'] = Variable<String>($MediaFilesTable
.$converterreuploadRequestedByn
@ -2642,6 +2685,7 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..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<bool> requiresAuthentication,
Value<bool> reopenByContact,
Value<bool> stored,
Value<bool> isDraftMedia,
Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds,
Value<bool?> removeAudio,
@ -9179,6 +9224,7 @@ typedef $$MediaFilesTableUpdateCompanionBuilder = MediaFilesCompanion Function({
Value<bool> requiresAuthentication,
Value<bool> reopenByContact,
Value<bool> stored,
Value<bool> isDraftMedia,
Value<List<int>?> reuploadRequestedBy,
Value<int?> displayLimitInMilliseconds,
Value<bool?> removeAudio,
@ -9248,6 +9294,9 @@ class $$MediaFilesTableFilterComposer
ColumnFilters<bool> get stored => $composableBuilder(
column: $table.stored, builder: (column) => ColumnFilters(column));
ColumnFilters<bool> get isDraftMedia => $composableBuilder(
column: $table.isDraftMedia, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<List<int>?, List<int>, String>
get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy,
@ -9331,6 +9380,10 @@ class $$MediaFilesTableOrderingComposer
ColumnOrderings<bool> get stored => $composableBuilder(
column: $table.stored, builder: (column) => ColumnOrderings(column));
ColumnOrderings<bool> get isDraftMedia => $composableBuilder(
column: $table.isDraftMedia,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy,
builder: (column) => ColumnOrderings(column));
@ -9394,6 +9447,9 @@ class $$MediaFilesTableAnnotationComposer
GeneratedColumn<bool> get stored =>
$composableBuilder(column: $table.stored, builder: (column) => column);
GeneratedColumn<bool> get isDraftMedia => $composableBuilder(
column: $table.isDraftMedia, builder: (column) => column);
GeneratedColumnWithTypeConverter<List<int>?, String>
get reuploadRequestedBy => $composableBuilder(
column: $table.reuploadRequestedBy, builder: (column) => column);
@ -9471,6 +9527,7 @@ class $$MediaFilesTableTableManager extends RootTableManager<
Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = const Value.absent(),
Value<bool> stored = const Value.absent(),
Value<bool> isDraftMedia = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> 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<bool> requiresAuthentication = const Value.absent(),
Value<bool> reopenByContact = const Value.absent(),
Value<bool> stored = const Value.absent(),
Value<bool> isDraftMedia = const Value.absent(),
Value<List<int>?> reuploadRequestedBy = const Value.absent(),
Value<int?> displayLimitInMilliseconds = const Value.absent(),
Value<bool?> 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,

View file

@ -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."
}

View file

@ -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."
}

View file

@ -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

View file

@ -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.';
}

View file

@ -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.';
}

View file

@ -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<Response_ProofOfWork?> 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<Result> downloadDone(List<int> token) async {

View file

@ -24,8 +24,24 @@ Future<void> 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<void> finishStartedPreprocessing() async {
Future<MediaFileService?> 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<void> insertMediaFileInMessagesTable(
MediaFileService mediaService,
List<String> groupIds,
) async {
await twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
isDraftMedia: Value(false),
),
);
for (final groupId in groupIds) {
final message = await twonlyDB.messagesDao.insertMessage(
MessagesCompanion(

View file

@ -65,20 +65,24 @@ Future<void> 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',

View file

@ -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',

View file

@ -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,

View file

@ -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<CameraPreviewView> {
final mediaFileService = await initializeMediaUpload(
type,
gUser.defaultShowTime,
isDraftMedia: true,
);
if (!mounted) return true;

View file

@ -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<ShareImageEditorView> {
} 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<ShareImageEditorView> {
isDisposed = true;
layers.clear();
videoController?.dispose();
twonlyDB.mediaFilesDao.updateAllMediaFiles(
const MediaFilesCompanion(
isDraftMedia: Value(false),
),
);
super.dispose();
}
@ -388,6 +397,10 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Future<void> loadImage(Future<Uint8List?> 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;

View file

@ -476,8 +476,8 @@ class _MediaViewerViewState extends State<MediaViewerView> {
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(

View file

@ -41,6 +41,12 @@ class _FlameCounterWidgetState extends State<FlameCounterWidget> {
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;

View file

@ -36,7 +36,8 @@ class _MaxFlameListTitleState extends State<MaxFlameListTitle> {
_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<MaxFlameListTitle> {
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<MaxFlameListTitle> {
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<MaxFlameListTitle> {
emoji: '🔥',
),
),
text: 'Restore your ${_directChat!.maxFlameCounter} lost flames',
text: 'Restore your ${_directChat!.maxFlameCounter + 1} lost flames',
);
}
}

View file

@ -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<HomeView> {
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

View file

@ -27,7 +27,7 @@ class RegisterView extends StatefulWidget {
});
final Function callbackOnSuccess;
final Future<int>? proofOfWork;
final (Future<int>?, bool) proofOfWork;
@override
State<RegisterView> createState() => _RegisterViewState();
}
@ -36,10 +36,20 @@ class _RegisterViewState extends State<RegisterView> {
final TextEditingController usernameController = TextEditingController();
final TextEditingController inviteCodeController = TextEditingController();
bool _registrationDisabled = false;
bool _isTryingToRegister = false;
bool _isValidUserName = false;
bool _showUserNameError = false;
late Future<int>? proofOfWork;
@override
void initState() {
proofOfWork = widget.proofOfWork.$1;
_registrationDisabled = widget.proofOfWork.$2;
super.initState();
}
Future<void> createNewUser() async {
if (!_isValidUserName) {
setState(() {
@ -57,11 +67,12 @@ class _RegisterViewState extends State<RegisterView> {
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<RegisterView> {
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<RegisterView> {
@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]);
}