mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-04-16 06:32:54 +00:00
Merge pull request #394 from twonlyapp/dev
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
Some checks failed
Publish on Github / build_and_publish (push) Has been cancelled
- New: Groups can now collect flames as well - New: Background execution to pre-load messages - New: Adds a link if the image contains a QR code - Improve: Video compression with progress updates - Improve: Show message "Flames restored" - Improve: Show toast message if user was added via QR - Fix: Media file appears as a white square and is not listed. - Fix: Issue with media files required to be reuploaded - Fix: Problem during contact requests - Fix: Problem with deleting a contact - Fix: Problem with restoring from backup - Fix: Issue with the log file
This commit is contained in:
commit
e953d1ffd2
9 changed files with 180 additions and 137 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Changelog
|
||||
|
||||
## 0.0.99
|
||||
## 0.1.1
|
||||
|
||||
- New: Groups can now collect flames as well
|
||||
- New: Background execution to pre-load messages
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
|
||||
final rowId = await into(mediaFiles).insert(insertMediaFile);
|
||||
|
||||
return await (select(mediaFiles)..where((t) => t.rowId.equals(rowId)))
|
||||
.getSingle();
|
||||
return await (select(
|
||||
mediaFiles,
|
||||
)..where((t) => t.rowId.equals(rowId))).getSingle();
|
||||
} catch (e) {
|
||||
Log.error('Could not insert media file: $e');
|
||||
return null;
|
||||
|
|
@ -35,10 +36,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
}
|
||||
|
||||
Future<void> deleteMediaFile(String mediaId) async {
|
||||
await (delete(mediaFiles)
|
||||
..where(
|
||||
(t) => t.mediaId.equals(mediaId),
|
||||
))
|
||||
await (delete(mediaFiles)..where(
|
||||
(t) => t.mediaId.equals(mediaId),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
|
|
@ -46,8 +46,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
String mediaId,
|
||||
MediaFilesCompanion updates,
|
||||
) async {
|
||||
await (update(mediaFiles)..where((c) => c.mediaId.equals(mediaId)))
|
||||
.write(updates);
|
||||
await (update(
|
||||
mediaFiles,
|
||||
)..where((c) => c.mediaId.equals(mediaId))).write(updates);
|
||||
}
|
||||
|
||||
Future<void> updateAllMediaFiles(
|
||||
|
|
@ -57,14 +58,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
}
|
||||
|
||||
Future<MediaFile?> getMediaFileById(String mediaId) async {
|
||||
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
|
||||
.getSingleOrNull();
|
||||
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();
|
||||
final medias = await (select(
|
||||
mediaFiles,
|
||||
)..where((t) => t.isDraftMedia.equals(true))).get();
|
||||
if (medias.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -72,65 +74,62 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
}
|
||||
|
||||
Stream<MediaFile?> watchMedia(String mediaId) {
|
||||
return (select(mediaFiles)..where((t) => t.mediaId.equals(mediaId)))
|
||||
.watchSingleOrNull();
|
||||
return (select(
|
||||
mediaFiles,
|
||||
)..where((t) => t.mediaId.equals(mediaId))).watchSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> resetPendingDownloadState() async {
|
||||
await (update(mediaFiles)
|
||||
..where(
|
||||
(c) => c.downloadState.equals(
|
||||
DownloadState.downloading.name,
|
||||
),
|
||||
))
|
||||
await (update(mediaFiles)..where(
|
||||
(c) => c.downloadState.equals(
|
||||
DownloadState.downloading.name,
|
||||
),
|
||||
))
|
||||
.write(
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.pending),
|
||||
),
|
||||
);
|
||||
const MediaFilesCompanion(
|
||||
downloadState: Value(DownloadState.pending),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllMediaFilesPendingDownload() async {
|
||||
return (select(mediaFiles)
|
||||
..where(
|
||||
(t) =>
|
||||
t.downloadState.equals(DownloadState.pending.name) |
|
||||
t.downloadState.equals(DownloadState.downloading.name),
|
||||
))
|
||||
return (select(mediaFiles)..where(
|
||||
(t) =>
|
||||
t.downloadState.equals(DownloadState.pending.name) |
|
||||
t.downloadState.equals(DownloadState.downloading.name),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllMediaFilesReuploadRequested() async {
|
||||
return (select(mediaFiles)
|
||||
..where(
|
||||
(t) => t.downloadState.equals(DownloadState.reuploadRequested.name),
|
||||
))
|
||||
return (select(mediaFiles)..where(
|
||||
(t) => t.downloadState.equals(DownloadState.reuploadRequested.name),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllNonHashedStoredMediaFiles() async {
|
||||
return (select(mediaFiles)
|
||||
..where(
|
||||
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
|
||||
))
|
||||
return (select(mediaFiles)..where(
|
||||
(t) => t.stored.equals(true) & t.storedFileHash.isNull(),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<MediaFile>> getAllMediaFilesPendingUpload() async {
|
||||
return (select(mediaFiles)
|
||||
..where(
|
||||
(t) => (t.uploadState.equals(UploadState.initialized.name) |
|
||||
t.uploadState.equals(UploadState.uploadLimitReached.name) |
|
||||
t.uploadState.equals(UploadState.uploading.name) |
|
||||
t.uploadState.equals(UploadState.preprocessing.name)),
|
||||
))
|
||||
return (select(mediaFiles)..where(
|
||||
(t) =>
|
||||
(t.uploadState.equals(UploadState.initialized.name) |
|
||||
t.uploadState.equals(UploadState.uploadLimitReached.name) |
|
||||
t.uploadState.equals(UploadState.uploading.name) |
|
||||
t.uploadState.equals(UploadState.preprocessing.name)),
|
||||
))
|
||||
.get();
|
||||
}
|
||||
|
||||
Stream<List<MediaFile>> watchAllStoredMediaFiles() {
|
||||
final query = (select(mediaFiles)..where((t) => t.stored.equals(true)))
|
||||
.join([])
|
||||
..groupBy([mediaFiles.storedFileHash]);
|
||||
final query =
|
||||
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
|
||||
..groupBy([mediaFiles.storedFileHash]);
|
||||
return query.map((row) => row.readTable(mediaFiles)).watch();
|
||||
}
|
||||
|
||||
|
|
@ -142,16 +141,15 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
|||
}
|
||||
|
||||
Future<void> updateAllRetransmissionUploadingState() async {
|
||||
await (update(mediaFiles)
|
||||
..where(
|
||||
(t) =>
|
||||
t.uploadState.equals(UploadState.uploading.name) &
|
||||
t.reuploadRequestedBy.isNotNull(),
|
||||
))
|
||||
await (update(mediaFiles)..where(
|
||||
(t) =>
|
||||
t.uploadState.equals(UploadState.uploading.name) &
|
||||
t.reuploadRequestedBy.isNotNull(),
|
||||
))
|
||||
.write(
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.preprocessing),
|
||||
),
|
||||
);
|
||||
const MediaFilesCompanion(
|
||||
uploadState: Value(UploadState.preprocessing),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,14 +124,15 @@ Future<void> handleContactUpdate(
|
|||
await twonlyDB.contactsDao.updateContact(
|
||||
fromUserId,
|
||||
ContactsCompanion(
|
||||
avatarSvgCompressed:
|
||||
Value(Uint8List.fromList(contactUpdate.avatarSvgCompressed)),
|
||||
avatarSvgCompressed: Value(
|
||||
Uint8List.fromList(contactUpdate.avatarSvgCompressed),
|
||||
),
|
||||
displayName: Value(contactUpdate.displayName),
|
||||
username: Value(contactUpdate.username),
|
||||
senderProfileCounter: Value(senderProfileCounter),
|
||||
),
|
||||
);
|
||||
unawaited(createPushAvatars());
|
||||
unawaited(createPushAvatars(forceForUserId: fromUserId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,10 +30,9 @@ Future<void> finishStartedPreprocessing() async {
|
|||
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||
.getAllMediaFilesPendingUpload();
|
||||
|
||||
Log.info('There are ${mediaFiles.length} media files pending');
|
||||
|
||||
for (final mediaFile in mediaFiles) {
|
||||
if (mediaFile.isDraftMedia) {
|
||||
Log.info('Ignoring media files as it is a draft');
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
|
|
@ -51,6 +50,9 @@ Future<void> finishStartedPreprocessing() async {
|
|||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||
continue;
|
||||
}
|
||||
Log.info(
|
||||
'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.',
|
||||
);
|
||||
await startBackgroundMediaUpload(service);
|
||||
} catch (e) {
|
||||
Log.warn(e);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
|
|
@ -6,6 +7,7 @@ import 'package:twonly/src/constants/keyvalue.keys.dart';
|
|||
import 'package:twonly/src/database/twonly.db.dart';
|
||||
import 'package:twonly/src/services/api.service.dart';
|
||||
import 'package:twonly/src/services/api/mediafiles/upload.service.dart';
|
||||
import 'package:twonly/src/utils/exclusive_access.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.dart';
|
||||
import 'package:twonly/src/utils/log.dart';
|
||||
import 'package:twonly/src/utils/storage.dart';
|
||||
|
|
@ -14,12 +16,14 @@ import 'package:workmanager/workmanager.dart';
|
|||
// ignore: unreachable_from_main
|
||||
Future<void> initializeBackgroundTaskManager() async {
|
||||
await Workmanager().initialize(callbackDispatcher);
|
||||
await Workmanager().cancelByUniqueName('fetch_data_from_server');
|
||||
|
||||
await Workmanager().registerPeriodicTask(
|
||||
'fetch_data_from_server',
|
||||
'eu.twonly.periodic_task',
|
||||
frequency: const Duration(minutes: 20),
|
||||
initialDelay: const Duration(minutes: 5),
|
||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
||||
constraints: Constraints(
|
||||
networkType: NetworkType.connected,
|
||||
),
|
||||
|
|
@ -47,16 +51,16 @@ void callbackDispatcher() {
|
|||
|
||||
Future<bool> initBackgroundExecution() async {
|
||||
SentryWidgetsFlutterBinding.ensureInitialized();
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
|
||||
initLogger();
|
||||
|
||||
final user = await getUser();
|
||||
if (user == null) return false;
|
||||
gUser = user;
|
||||
|
||||
globalApplicationCacheDirectory = (await getApplicationCacheDirectory()).path;
|
||||
globalApplicationSupportDirectory =
|
||||
(await getApplicationSupportDirectory()).path;
|
||||
|
||||
twonlyDB = TwonlyDB();
|
||||
apiService = ApiService();
|
||||
globalIsInBackgroundTask = true;
|
||||
|
|
@ -64,26 +68,35 @@ Future<bool> initBackgroundExecution() async {
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<bool> handlePeriodicTask() async {
|
||||
final lastExecution =
|
||||
await KeyValueStore.get(KeyValueKeys.lastPeriodicTaskExecution);
|
||||
if (lastExecution == null || !lastExecution.containsKey('timestamp')) {
|
||||
final lastExecutionTime = lastExecution?['timestamp'] as int?;
|
||||
if (lastExecutionTime != null) {
|
||||
final lastExecution =
|
||||
DateTime.fromMillisecondsSinceEpoch(lastExecutionTime);
|
||||
if (DateTime.now().difference(lastExecution).inMinutes < 2) {
|
||||
Log.info(
|
||||
'eu.twonly.periodic_task not executed as last execution was within the last two minutes.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
final Mutex _keyValueMutex = Mutex();
|
||||
|
||||
await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, {
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
Future<void> handlePeriodicTask() async {
|
||||
final shouldBeExecuted = await exclusiveAccess(
|
||||
lockName: 'periodic_task',
|
||||
mutex: _keyValueMutex,
|
||||
action: () async {
|
||||
final lastExecution = await KeyValueStore.get(
|
||||
KeyValueKeys.lastPeriodicTaskExecution,
|
||||
);
|
||||
if (lastExecution != null && lastExecution.containsKey('timestamp')) {
|
||||
final lastExecutionTime = lastExecution['timestamp'] as int?;
|
||||
if (lastExecutionTime != null) {
|
||||
final lastExecutionDate = DateTime.fromMillisecondsSinceEpoch(
|
||||
lastExecutionTime,
|
||||
);
|
||||
if (DateTime.now().difference(lastExecutionDate).inMinutes < 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
await KeyValueStore.put(KeyValueKeys.lastPeriodicTaskExecution, {
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
if (!shouldBeExecuted) return;
|
||||
|
||||
Log.info('eu.twonly.periodic_task was called.');
|
||||
|
||||
|
|
@ -91,12 +104,12 @@ Future<bool> handlePeriodicTask() async {
|
|||
|
||||
if (!await apiService.connect()) {
|
||||
Log.info('Could not connect to the api. Returning early.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiService.isAuthenticated) {
|
||||
Log.info('Api is not authenticated. Returning early.');
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
while (!globalGotMessageFromServer) {
|
||||
|
|
@ -119,7 +132,7 @@ Future<bool> handlePeriodicTask() async {
|
|||
stopwatch.stop();
|
||||
|
||||
Log.info('eu.twonly.periodic_task finished after ${stopwatch.elapsed}.');
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> handleProcessingTask() async {
|
||||
|
|
|
|||
|
|
@ -6,12 +6,21 @@ import 'package:flutter_svg/svg.dart';
|
|||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/misc.dart';
|
||||
|
||||
Future<void> createPushAvatars() async {
|
||||
Future<void> createPushAvatars({int? forceForUserId}) async {
|
||||
final contacts = await twonlyDB.contactsDao.getAllContacts();
|
||||
|
||||
for (final contact in contacts) {
|
||||
if (contact.avatarSvgCompressed == null) continue;
|
||||
|
||||
if (forceForUserId == null) {
|
||||
if (avatarPNGFile(contact.userId).existsSync()) {
|
||||
continue; // only create the avatar in case no avatar exists yet fot this user
|
||||
}
|
||||
} else if (contact.userId != forceForUserId) {
|
||||
// only update the avatar for this specified contact
|
||||
continue;
|
||||
}
|
||||
|
||||
final avatarSvg = getAvatarSvg(contact.avatarSvgCompressed!);
|
||||
|
||||
final pictureInfo = await vg.loadPicture(SvgStringLoader(avatarSvg), null);
|
||||
|
|
@ -27,8 +36,9 @@ Future<void> createPushAvatars() async {
|
|||
}
|
||||
|
||||
File avatarPNGFile(int contactId) {
|
||||
final avatarsDirectory =
|
||||
Directory('$globalApplicationCacheDirectory/avatars');
|
||||
final avatarsDirectory = Directory(
|
||||
'$globalApplicationCacheDirectory/avatars',
|
||||
);
|
||||
|
||||
if (!avatarsDirectory.existsSync()) {
|
||||
avatarsDirectory.createSync(recursive: true);
|
||||
|
|
@ -42,8 +52,10 @@ Future<Uint8List> getUserAvatar() async {
|
|||
return data.buffer.asUint8List();
|
||||
}
|
||||
|
||||
final pictureInfo =
|
||||
await vg.loadPicture(SvgStringLoader(gUser.avatarSvg!), null);
|
||||
final pictureInfo = await vg.loadPicture(
|
||||
SvgStringLoader(gUser.avatarSvg!),
|
||||
null,
|
||||
);
|
||||
|
||||
final image = await pictureInfo.picture.toImage(270, 300);
|
||||
|
||||
|
|
|
|||
51
lib/src/utils/exclusive_access.dart
Normal file
51
lib/src/utils/exclusive_access.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
|
||||
Future<T> exclusiveAccess<T>({
|
||||
required String lockName,
|
||||
required Future<T> Function() action,
|
||||
required Mutex mutex,
|
||||
}) async {
|
||||
final lockFile = File('$globalApplicationSupportDirectory/$lockName.lock');
|
||||
return mutex.protect(() async {
|
||||
var lockAcquired = false;
|
||||
|
||||
while (!lockAcquired) {
|
||||
try {
|
||||
lockFile.createSync(exclusive: true);
|
||||
lockAcquired = true;
|
||||
} on FileSystemException catch (e) {
|
||||
final isExists = e is PathExistsException || e.osError?.errorCode == 17;
|
||||
if (!isExists) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
final stat = lockFile.statSync();
|
||||
if (stat.type != FileSystemEntityType.notFound) {
|
||||
final age = DateTime.now().difference(stat.modified).inMilliseconds;
|
||||
if (age > 1000) {
|
||||
lockFile.deleteSync();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
} catch (_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
if (lockFile.existsSync()) {
|
||||
lockFile.deleteSync();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
|
|||
import 'package:mutex/mutex.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:twonly/globals.dart';
|
||||
import 'package:twonly/src/utils/exclusive_access.dart';
|
||||
|
||||
void initLogger() {
|
||||
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
|
||||
|
|
@ -91,46 +92,11 @@ Future<String> readLast1000Lines() async {
|
|||
final Mutex _logMutex = Mutex();
|
||||
|
||||
Future<T> _protectFileAccess<T>(Future<T> Function() action) async {
|
||||
return _logMutex.protect(() async {
|
||||
final lockFile = File('$globalApplicationSupportDirectory/app.log.lock');
|
||||
var lockAcquired = false;
|
||||
|
||||
while (!lockAcquired) {
|
||||
try {
|
||||
lockFile.createSync(exclusive: true);
|
||||
lockAcquired = true;
|
||||
} on FileSystemException catch (e) {
|
||||
final isExists = e is PathExistsException || e.osError?.errorCode == 17;
|
||||
if (!isExists) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
final stat = lockFile.statSync();
|
||||
if (stat.type != FileSystemEntityType.notFound) {
|
||||
final age = DateTime.now().difference(stat.modified).inMilliseconds;
|
||||
if (age > 1000) {
|
||||
lockFile.deleteSync();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
} catch (_) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
if (lockAcquired) {
|
||||
try {
|
||||
if (lockFile.existsSync()) {
|
||||
lockFile.deleteSync();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
return exclusiveAccess(
|
||||
lockName: 'app.log',
|
||||
action: action,
|
||||
mutex: _logMutex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _writeLogToFile(LogRecord record) async {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
|||
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.0.99+99
|
||||
version: 0.1.1+101
|
||||
|
||||
environment:
|
||||
sdk: ^3.11.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue