mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-24 23:52:11 +00:00
improve startup
This commit is contained in:
parent
281014133a
commit
f553713ff8
13 changed files with 294 additions and 208 deletions
|
|
@ -1,5 +1,9 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.3
|
||||||
|
|
||||||
|
- Fix: App did not launch sometimes on Android
|
||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
- New: Feature to find friends without a phone number
|
- New: Feature to find friends without a phone number
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ analyzer:
|
||||||
- "lib/generated/**"
|
- "lib/generated/**"
|
||||||
- "lib/core/**"
|
- "lib/core/**"
|
||||||
- "lib/src/localization/**"
|
- "lib/src/localization/**"
|
||||||
|
- "rust_builder/"
|
||||||
- "dependencies/**"
|
- "dependencies/**"
|
||||||
- "pubspec.yaml"
|
- "pubspec.yaml"
|
||||||
- "**.arb"
|
- "**.arb"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ Future<void> twonlyMinimumInitialization() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
await twonlyMinimumInitialization();
|
await twonlyMinimumInitialization();
|
||||||
|
|
||||||
unawaited(initFCMService());
|
unawaited(initFCMService());
|
||||||
|
|
@ -67,12 +68,9 @@ void main() async {
|
||||||
storageError = true;
|
storageError = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final dbExists = File(
|
|
||||||
'${AppEnvironment.supportDir}/twonly.sqlite',
|
|
||||||
).existsSync();
|
|
||||||
|
|
||||||
if (Platform.isIOS && userExists) {
|
if (Platform.isIOS && userExists) {
|
||||||
if (!dbExists) {
|
final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite');
|
||||||
|
if (!dbFile.existsSync()) {
|
||||||
Log.error('[twonly] IOS: App was removed and then reinstalled again...');
|
Log.error('[twonly] IOS: App was removed and then reinstalled again...');
|
||||||
await SecureStorage.instance.deleteAll();
|
await SecureStorage.instance.deleteAll();
|
||||||
userExists = false;
|
userExists = false;
|
||||||
|
|
@ -81,7 +79,7 @@ void main() async {
|
||||||
|
|
||||||
final settingsController = SettingsChangeProvider()..loadSettings();
|
final settingsController = SettingsChangeProvider()..loadSettings();
|
||||||
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||||
await initFileDownloader();
|
unawaited(initFileDownloader());
|
||||||
|
|
||||||
if (userExists) {
|
if (userExists) {
|
||||||
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
if (userService.currentUser.allowErrorTrackingViaSentry) {
|
||||||
|
|
@ -96,23 +94,16 @@ void main() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
|
unawaited(postStartupTasks());
|
||||||
await twonlyDB.messagesDao.purgeMessageTable();
|
|
||||||
await twonlyDB.receiptsDao.purgeReceivedReceipts();
|
|
||||||
await UserDiscoveryService.removeDeletedContacts();
|
|
||||||
|
|
||||||
unawaited(MediaFileService.purgeTempFolder());
|
|
||||||
|
|
||||||
unawaited(setupPushNotification());
|
|
||||||
unawaited(finishStartedPreprocessing());
|
|
||||||
unawaited(createPushAvatars());
|
|
||||||
unawaited(performTwonlySafeBackup());
|
|
||||||
unawaited(initializeBackgroundTaskManager());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await apiService.listenToNetworkChanges();
|
await apiService.listenToNetworkChanges();
|
||||||
unawaited(apiService.connect());
|
unawaited(apiService.connect());
|
||||||
|
|
||||||
|
stopwatch.stop();
|
||||||
|
|
||||||
|
Log.info('Initialization finished after ${stopwatch.elapsed}.');
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
|
|
@ -161,3 +152,21 @@ Future<void> runMigrations() async {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> postStartupTasks() async {
|
||||||
|
// 1. Immediate background cleanup (Non-blocking for UI)
|
||||||
|
await twonlyDB.messagesDao.purgeMessageTable();
|
||||||
|
unawaited(twonlyDB.receiptsDao.purgeReceivedReceipts());
|
||||||
|
unawaited(UserDiscoveryService.removeDeletedContacts());
|
||||||
|
unawaited(MediaFileService.purgeTempFolder());
|
||||||
|
|
||||||
|
// 2. Service initializations
|
||||||
|
unawaited(setupPushNotification());
|
||||||
|
unawaited(finishStartedPreprocessing());
|
||||||
|
unawaited(createPushAvatars());
|
||||||
|
unawaited(initializeBackgroundTaskManager());
|
||||||
|
|
||||||
|
// 3. Delayed tasks (Wait for app to settle)
|
||||||
|
await Future.delayed(const Duration(minutes: 2));
|
||||||
|
unawaited(performTwonlySafeBackup());
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ class LoggingCallbacks {
|
||||||
print(log);
|
print(log);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDone: () => Log.info('Log stream closed'),
|
|
||||||
);
|
);
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,11 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
|
||||||
)..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull();
|
)..where((t) => t.mediaId.equals(mediaId))).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<MediaFile>> getMediaFilesByIds(List<String> mediaIds) async {
|
||||||
|
return (select(mediaFiles)..where((t) => t.mediaId.isIn(mediaIds))).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<MediaFile?> getDraftMediaFile() async {
|
Future<MediaFile?> getDraftMediaFile() async {
|
||||||
final medias = await (select(
|
final medias = await (select(
|
||||||
mediaFiles,
|
mediaFiles,
|
||||||
|
|
|
||||||
|
|
@ -140,15 +140,22 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
Future<void> purgeMessageTable() async {
|
Future<void> purgeMessageTable() async {
|
||||||
final allGroups = await select(groups).get();
|
final allGroups = await select(groups).get();
|
||||||
|
|
||||||
for (final group in allGroups) {
|
final groupedByTime = <int, List<String>>{};
|
||||||
|
for (final g in allGroups) {
|
||||||
|
groupedByTime
|
||||||
|
.putIfAbsent(g.deleteMessagesAfterMilliseconds, () => [])
|
||||||
|
.add(g.groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final entry in groupedByTime.entries) {
|
||||||
final deletionTime = clock.now().subtract(
|
final deletionTime = clock.now().subtract(
|
||||||
Duration(
|
Duration(milliseconds: entry.key),
|
||||||
milliseconds: group.deleteMessagesAfterMilliseconds,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
final groupIds = entry.value;
|
||||||
|
|
||||||
await (delete(messages)..where(
|
await (delete(messages)..where(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.groupId.equals(group.groupId) &
|
m.groupId.isIn(groupIds) &
|
||||||
(m.mediaStored.equals(true) &
|
(m.mediaStored.equals(true) &
|
||||||
m.isDeletedFromSender.equals(true) |
|
m.isDeletedFromSender.equals(true) |
|
||||||
m.mediaStored.equals(false)) &
|
m.mediaStored.equals(false)) &
|
||||||
|
|
@ -404,6 +411,10 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
|
||||||
return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get();
|
return (select(messages)..where((t) => t.mediaId.equals(mediaId))).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Message>> getMessagesByMediaIds(List<String> mediaIds) async {
|
||||||
|
return (select(messages)..where((t) => t.mediaId.isIn(mediaIds))).get();
|
||||||
|
}
|
||||||
|
|
||||||
Stream<List<(MessageAction, Contact)>> watchMessageActions(String messageId) {
|
Stream<List<(MessageAction, Contact)>> watchMessageActions(String messageId) {
|
||||||
final query = (select(messageActions).join([
|
final query = (select(messageActions).join([
|
||||||
leftOuterJoin(
|
leftOuterJoin(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import 'package:twonly/src/services/api/mediafiles/media_background.api.dart';
|
||||||
import 'package:twonly/src/services/api/messages.api.dart';
|
import 'package:twonly/src/services/api/messages.api.dart';
|
||||||
import 'package:twonly/src/services/flame.service.dart';
|
import 'package:twonly/src/services/flame.service.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
|
import 'package:twonly/src/utils/exclusive_access.utils.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/secure_storage.dart';
|
import 'package:twonly/src/utils/secure_storage.dart';
|
||||||
|
|
@ -31,124 +32,128 @@ import 'package:workmanager/workmanager.dart' hide TaskStatus;
|
||||||
final lockRetransmission = Mutex();
|
final lockRetransmission = Mutex();
|
||||||
|
|
||||||
Future<void> reuploadMediaFiles() async {
|
Future<void> reuploadMediaFiles() async {
|
||||||
return lockRetransmission.protect(() async {
|
return exclusiveAccess(
|
||||||
final receipts = await twonlyDB.receiptsDao
|
lockName: 'reupload_maintenance',
|
||||||
.getReceiptsForMediaRetransmissions();
|
mutex: lockRetransmission,
|
||||||
|
action: () async {
|
||||||
|
final receipts = await twonlyDB.receiptsDao
|
||||||
|
.getReceiptsForMediaRetransmissions();
|
||||||
|
|
||||||
if (receipts.isEmpty) return;
|
if (receipts.isEmpty) return;
|
||||||
|
|
||||||
Log.info('Reuploading ${receipts.length} media files to the server.');
|
Log.info('Reuploading ${receipts.length} media files to the server.');
|
||||||
|
|
||||||
final contacts = <int, Contact>{};
|
final contacts = <int, Contact>{};
|
||||||
|
|
||||||
for (final receipt in receipts) {
|
for (final receipt in receipts) {
|
||||||
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
|
if (receipt.retryCount > 1 && receipt.lastRetry != null) {
|
||||||
final twentyFourHoursAgo = DateTime.now().subtract(
|
final twentyFourHoursAgo = DateTime.now().subtract(
|
||||||
const Duration(hours: 24),
|
const Duration(hours: 24),
|
||||||
);
|
|
||||||
if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) {
|
|
||||||
Log.info(
|
|
||||||
'Ignoring ${receipt.receiptId} as it was retried in the last 24h',
|
|
||||||
);
|
);
|
||||||
continue;
|
if (receipt.lastRetry!.isAfter(twentyFourHoursAgo)) {
|
||||||
}
|
Log.info(
|
||||||
}
|
'Ignoring ${receipt.receiptId} as it was retried in the last 24h',
|
||||||
var messageId = receipt.messageId;
|
|
||||||
if (receipt.messageId == null) {
|
|
||||||
Log.info('Message not in receipt. Loading it from the content.');
|
|
||||||
try {
|
|
||||||
final content = EncryptedContent.fromBuffer(receipt.message);
|
|
||||||
if (content.hasMedia()) {
|
|
||||||
messageId = content.media.senderMessageId;
|
|
||||||
final messageExists = await twonlyDB.messagesDao
|
|
||||||
.getMessageById(messageId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (messageExists != null) {
|
|
||||||
await twonlyDB.receiptsDao.updateReceipt(
|
|
||||||
receipt.receiptId,
|
|
||||||
ReceiptsCompanion(
|
|
||||||
messageId: Value(messageId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.info(
|
|
||||||
'Message $messageId not found in DB for receipt recovery. Deleting stale receipt.',
|
|
||||||
);
|
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
Log.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (messageId == null) {
|
|
||||||
Log.error('MessageId is empty for media file receipts');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (receipt.markForRetryAfterAccepted != null) {
|
|
||||||
if (!contacts.containsKey(receipt.contactId)) {
|
|
||||||
final contact = await twonlyDB.contactsDao
|
|
||||||
.getContactByUserId(receipt.contactId)
|
|
||||||
.getSingleOrNull();
|
|
||||||
if (contact == null) {
|
|
||||||
Log.error(
|
|
||||||
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
|
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
contacts[receipt.contactId] = contact;
|
|
||||||
}
|
}
|
||||||
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
|
var messageId = receipt.messageId;
|
||||||
Log.warn(
|
if (receipt.messageId == null) {
|
||||||
'Could not send message as contact has still not yet accepted.',
|
Log.info('Message not in receipt. Loading it from the content.');
|
||||||
);
|
try {
|
||||||
continue;
|
final content = EncryptedContent.fromBuffer(receipt.message);
|
||||||
}
|
if (content.hasMedia()) {
|
||||||
}
|
messageId = content.media.senderMessageId;
|
||||||
|
final messageExists = await twonlyDB.messagesDao
|
||||||
if (receipt.ackByServerAt == null) {
|
.getMessageById(messageId)
|
||||||
// media file must be reuploaded again in case the media files
|
.getSingleOrNull();
|
||||||
// was deleted by the server, the receiver will request a new media reupload
|
if (messageExists != null) {
|
||||||
|
await twonlyDB.receiptsDao.updateReceipt(
|
||||||
final message = await twonlyDB.messagesDao
|
receipt.receiptId,
|
||||||
.getMessageById(messageId)
|
ReceiptsCompanion(
|
||||||
.getSingleOrNull();
|
messageId: Value(messageId),
|
||||||
if (message == null || message.mediaId == null) {
|
),
|
||||||
// The message or media file does not exists any more, so delete the receipt...
|
);
|
||||||
if (message != null) {
|
} else {
|
||||||
// The media file of the message does not exist anymore. Removing it...
|
Log.info(
|
||||||
await twonlyDB.messagesDao.deleteMessagesById(messageId);
|
'Message $messageId not found in DB for receipt recovery. Deleting stale receipt.',
|
||||||
|
);
|
||||||
|
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
}
|
}
|
||||||
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
}
|
||||||
Log.warn(
|
if (messageId == null) {
|
||||||
'Message not found for reupload of the receipt, likely deleted from sender (${message == null} - ${message?.mediaId}).',
|
Log.error('MessageId is empty for media file receipts');
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (receipt.markForRetryAfterAccepted != null) {
|
||||||
|
if (!contacts.containsKey(receipt.contactId)) {
|
||||||
|
final contact = await twonlyDB.contactsDao
|
||||||
|
.getContactByUserId(receipt.contactId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (contact == null) {
|
||||||
|
Log.error(
|
||||||
|
'Contact does not exists, but has a record in receipts, this should not be possible, because of the DELETE CASCADE relation.',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
contacts[receipt.contactId] = contact;
|
||||||
|
}
|
||||||
|
if (!(contacts[receipt.contactId]?.accepted ?? true)) {
|
||||||
|
Log.warn(
|
||||||
|
'Could not send message as contact has still not yet accepted.',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
if (receipt.ackByServerAt == null) {
|
||||||
message.mediaId!,
|
// media file must be reuploaded again in case the media files
|
||||||
);
|
// was deleted by the server, the receiver will request a new media reupload
|
||||||
if (mediaFile == null) {
|
|
||||||
Log.error(
|
final message = await twonlyDB.messagesDao
|
||||||
'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).',
|
.getMessageById(messageId)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (message == null || message.mediaId == null) {
|
||||||
|
// The message or media file does not exists any more, so delete the receipt...
|
||||||
|
if (message != null) {
|
||||||
|
// The media file of the message does not exist anymore. Removing it...
|
||||||
|
await twonlyDB.messagesDao.deleteMessagesById(messageId);
|
||||||
|
}
|
||||||
|
await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId);
|
||||||
|
Log.warn(
|
||||||
|
'Message not found for reupload of the receipt, likely deleted from sender (${message == null} - ${message?.mediaId}).',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
|
||||||
|
message.mediaId!,
|
||||||
);
|
);
|
||||||
continue;
|
if (mediaFile == null) {
|
||||||
|
Log.error(
|
||||||
|
'Mediafile not found for reupload of the receipt (${message.messageId} - ${message.mediaId}).',
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await reuploadMediaFile(
|
||||||
|
receipt.contactId,
|
||||||
|
mediaFile,
|
||||||
|
message.messageId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Log.info('Reuploading media file $messageId');
|
||||||
|
// the media file should be still on the server, so it should be enough
|
||||||
|
// to just resend the message containing the download token.
|
||||||
|
await tryToSendCompleteMessage(receipt: receipt);
|
||||||
}
|
}
|
||||||
await reuploadMediaFile(
|
|
||||||
receipt.contactId,
|
|
||||||
mediaFile,
|
|
||||||
message.messageId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.info('Reuploading media file $messageId');
|
|
||||||
// the media file should be still on the server, so it should be enough
|
|
||||||
// to just resend the message containing the download token.
|
|
||||||
await tryToSendCompleteMessage(receipt: receipt);
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> reuploadMediaFile(
|
Future<void> reuploadMediaFile(
|
||||||
|
|
@ -187,69 +192,77 @@ Future<void> reuploadMediaFile(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Mutex _lockPreprocessing = Mutex();
|
||||||
|
|
||||||
Future<void> finishStartedPreprocessing() async {
|
Future<void> finishStartedPreprocessing() async {
|
||||||
final mediaFiles = await twonlyDB.mediaFilesDao
|
return exclusiveAccess(
|
||||||
.getAllMediaFilesPendingUpload();
|
lockName: 'preprocessing_maintenance',
|
||||||
|
mutex: _lockPreprocessing,
|
||||||
|
action: () async {
|
||||||
|
final mediaFiles = await twonlyDB.mediaFilesDao
|
||||||
|
.getAllMediaFilesPendingUpload();
|
||||||
|
|
||||||
for (final mediaFile in mediaFiles) {
|
for (final mediaFile in mediaFiles) {
|
||||||
if (mediaFile.isDraftMedia) {
|
if (mediaFile.isDraftMedia) {
|
||||||
Log.info('Ignoring media files as it is a draft');
|
Log.info('Ignoring media files as it is a draft');
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final service = MediaFileService(mediaFile);
|
|
||||||
if (!service.originalPath.existsSync() &&
|
|
||||||
!service.uploadRequestPath.existsSync()) {
|
|
||||||
if (service.storedPath.existsSync()) {
|
|
||||||
// media files was just stored..
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (mediaFile.reuploadRequestedBy != null) {
|
|
||||||
Log.warn(
|
|
||||||
'Reupload requested for ${mediaFile.mediaId} but files are missing. Cancelling reupload but keeping record.',
|
|
||||||
);
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
uploadState: Value(UploadState.uploaded),
|
|
||||||
reuploadRequestedBy: Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
final service = MediaFileService(mediaFile);
|
||||||
|
if (!service.originalPath.existsSync() &&
|
||||||
|
!service.uploadRequestPath.existsSync()) {
|
||||||
|
if (service.storedPath.existsSync()) {
|
||||||
|
// media files was just stored..
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mediaFile.reuploadRequestedBy != null) {
|
||||||
|
Log.warn(
|
||||||
|
'Reupload requested for ${mediaFile.mediaId} but files are missing. Cancelling reupload but keeping record.',
|
||||||
|
);
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(
|
||||||
|
uploadState: Value(UploadState.uploaded),
|
||||||
|
reuploadRequestedBy: Value(null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
||||||
mediaFile.mediaId,
|
mediaFile.mediaId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (messages.isEmpty) {
|
if (messages.isEmpty) {
|
||||||
|
Log.info(
|
||||||
|
'Deleted media files ${mediaFile.mediaId} as originalPath and uploadRequestPath both do not exists and no messages reference it.',
|
||||||
|
);
|
||||||
|
// the file does not exists anymore and no messages reference it.
|
||||||
|
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
||||||
|
} else {
|
||||||
|
Log.warn(
|
||||||
|
'Media files ${mediaFile.mediaId} missing but messages still reference it. Keeping record to avoid broken chat history.',
|
||||||
|
);
|
||||||
|
// Just mark as uploaded to stop preprocessing attempts
|
||||||
|
await twonlyDB.mediaFilesDao.updateMedia(
|
||||||
|
mediaFile.mediaId,
|
||||||
|
const MediaFilesCompanion(
|
||||||
|
uploadState: Value(UploadState.uploaded),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Log.info(
|
Log.info(
|
||||||
'Deleted media files ${mediaFile.mediaId} as originalPath and uploadRequestPath both do not exists and no messages reference it.',
|
'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.',
|
||||||
);
|
|
||||||
// the file does not exists anymore and no messages reference it.
|
|
||||||
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaFile.mediaId);
|
|
||||||
} else {
|
|
||||||
Log.warn(
|
|
||||||
'Media files ${mediaFile.mediaId} missing but messages still reference it. Keeping record to avoid broken chat history.',
|
|
||||||
);
|
|
||||||
// Just mark as uploaded to stop preprocessing attempts
|
|
||||||
await twonlyDB.mediaFilesDao.updateMedia(
|
|
||||||
mediaFile.mediaId,
|
|
||||||
const MediaFilesCompanion(
|
|
||||||
uploadState: Value(UploadState.uploaded),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
await startBackgroundMediaUpload(service);
|
||||||
|
} catch (e) {
|
||||||
|
Log.warn(e);
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
Log.info(
|
},
|
||||||
'Finishing started preprocessing of ${mediaFile.mediaId} in state ${mediaFile.uploadState}.',
|
);
|
||||||
);
|
|
||||||
await startBackgroundMediaUpload(service);
|
|
||||||
} catch (e) {
|
|
||||||
Log.warn(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
|
/// It can happen, that a media files is uploaded but not yet marked for been uploaded.
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ Future<void> initializeBackgroundTaskManager() async {
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void callbackDispatcher() {
|
void callbackDispatcher() {
|
||||||
Workmanager().executeTask((task, inputData) async {
|
Workmanager().executeTask((task, inputData) async {
|
||||||
|
AppState.isInBackgroundTask = true;
|
||||||
switch (task) {
|
switch (task) {
|
||||||
case 'eu.twonly.periodic_task':
|
case 'eu.twonly.periodic_task':
|
||||||
if (await initBackgroundExecution()) {
|
if (await initBackgroundExecution()) {
|
||||||
|
|
@ -63,8 +64,6 @@ Future<bool> initBackgroundExecution() async {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppState.isInBackgroundTask = true;
|
|
||||||
|
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,39 +33,58 @@ class MediaFileService {
|
||||||
);
|
);
|
||||||
|
|
||||||
final files = tempDirectory.listSync();
|
final files = tempDirectory.listSync();
|
||||||
|
if (files.isEmpty) return;
|
||||||
|
|
||||||
|
final mediaIdToFile = <String, List<FileSystemEntity>>{};
|
||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
final mediaId = basename(file.path).split('.').first;
|
final mediaId = basename(file.path).split('.').first;
|
||||||
|
mediaIdToFile.putIfAbsent(mediaId, () => []).add(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
final mediaIds = mediaIdToFile.keys.toList();
|
||||||
|
|
||||||
|
// Bulk fetch media files and messages
|
||||||
|
final allMediaFiles = await twonlyDB.mediaFilesDao.getMediaFilesByIds(
|
||||||
|
mediaIds,
|
||||||
|
);
|
||||||
|
final allMessages = await twonlyDB.messagesDao.getMessagesByMediaIds(
|
||||||
|
mediaIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
final mediaFileMap = {for (final m in allMediaFiles) m.mediaId: m};
|
||||||
|
final messageMap = <String, List<Message>>{};
|
||||||
|
for (final msg in allMessages) {
|
||||||
|
if (msg.mediaId != null) {
|
||||||
|
messageMap.putIfAbsent(msg.mediaId!, () => []).add(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final mediaId in mediaIds) {
|
||||||
// in case the mediaID is unknown the file will be deleted
|
// in case the mediaID is unknown the file will be deleted
|
||||||
var delete = true;
|
var delete = true;
|
||||||
|
|
||||||
final service = await MediaFileService.fromMediaId(mediaId);
|
final mediaFile = mediaFileMap[mediaId];
|
||||||
|
|
||||||
if (service != null) {
|
if (mediaFile != null) {
|
||||||
if (service.mediaFile.isDraftMedia) {
|
if (mediaFile.isDraftMedia) {
|
||||||
delete = false;
|
delete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final messages = await twonlyDB.messagesDao.getMessagesByMediaId(
|
final messages = messageMap[mediaId] ?? [];
|
||||||
mediaId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// in case messages in empty the file will be deleted, as delete is true by default
|
// in case messages in empty the file will be deleted, as delete is true by default
|
||||||
|
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
if (service.mediaFile.type == MediaType.audio) {
|
if (mediaFile.type == MediaType.audio) {
|
||||||
delete = false; // do not delete voice messages
|
delete = false; // do not delete voice messages
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.openedAt == null) {
|
if (message.openedAt == null) {
|
||||||
// Message was not yet opened from all persons, so wait...
|
// Message was not yet opened from all persons, so wait...
|
||||||
delete = false;
|
delete = false;
|
||||||
} else if (service.mediaFile.requiresAuthentication ||
|
} else if (mediaFile.requiresAuthentication ||
|
||||||
service.mediaFile.displayLimitInMilliseconds != null) {
|
mediaFile.displayLimitInMilliseconds != null) {
|
||||||
// Message was opened by all persons, and they can not reopen the image.
|
// Message was opened by all persons, and they can not reopen the image.
|
||||||
// This branch will prevent to reach the next if condition, with would otherwise store the image for two days
|
|
||||||
// delete = true; // do not overwrite a previous delete = false
|
|
||||||
// this is just to make it easier to understand :)
|
|
||||||
} else if (message.openedAt!.isAfter(
|
} else if (message.openedAt!.isAfter(
|
||||||
clock.now().subtract(const Duration(days: 2)),
|
clock.now().subtract(const Duration(days: 2)),
|
||||||
)) {
|
)) {
|
||||||
|
|
@ -89,11 +108,20 @@ class MediaFileService {
|
||||||
|
|
||||||
if (delete) {
|
if (delete) {
|
||||||
Log.info('Purging media file $mediaId');
|
Log.info('Purging media file $mediaId');
|
||||||
file.deleteSync();
|
final filesToPurge = mediaIdToFile[mediaId] ?? [];
|
||||||
|
for (final file in filesToPurge) {
|
||||||
|
try {
|
||||||
|
if (file.existsSync()) {
|
||||||
|
file.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Error deleting file ${file.path}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
Log.error('Error in purgeTempFolder: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
import 'package:twonly/src/database/daos/key_verification.dao.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
|
||||||
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
import 'package:twonly/src/visual/components/verification_badge_info.comp.dart';
|
||||||
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
import 'package:twonly/src/visual/elements/svg_icon.element.dart';
|
||||||
|
|
||||||
|
|
@ -65,7 +64,6 @@ class _VerificationBadgeCompState extends State<VerificationBadgeComp> {
|
||||||
.listen((update) {
|
.listen((update) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
Log.info('Update: ${update.length}');
|
|
||||||
_isVerified = update.isNotEmpty;
|
_isVerified = update.isNotEmpty;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:twonly/src/constants/routes.keys.dart';
|
import 'package:twonly/src/constants/routes.keys.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
|
|
@ -157,25 +158,39 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
|
||||||
final tsStyle = TextStyle(
|
final tsStyle = TextStyle(
|
||||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12,
|
||||||
);
|
);
|
||||||
final levelStyle = TextStyle(
|
final fileNameStyle = TextStyle(
|
||||||
color: Colors.blueGrey.shade600,
|
color: Colors.blueGrey.shade400,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 11,
|
||||||
);
|
);
|
||||||
final msgStyle = TextStyle(
|
final msgStyle = TextStyle(
|
||||||
color: isDarkMode(context) ? Colors.white : Colors.black,
|
color: isDarkMode(context) ? Colors.white : Colors.black,
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 13,
|
||||||
);
|
);
|
||||||
|
|
||||||
return TextSpan(
|
return TextSpan(
|
||||||
children: [
|
children: [
|
||||||
if (_showTimestamps && e.timestamp != null)
|
if (_showTimestamps && e.timestamp != null)
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '${e.timestamp} '.replaceAll('.000', ''),
|
text: '${e.timestamp.toString().split(' ')[1].split('.')[0]} ',
|
||||||
style: tsStyle,
|
style: tsStyle,
|
||||||
),
|
),
|
||||||
TextSpan(text: '${e.fileName}\n', style: levelStyle),
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4),
|
||||||
|
child: FaIcon(
|
||||||
|
e.isBackground ? FontAwesomeIcons.clock : FontAwesomeIcons.mobile,
|
||||||
|
size: 12,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(text: '${e.fileName}\n', style: fileNameStyle),
|
||||||
TextSpan(text: e.message, style: msgStyle),
|
TextSpan(text: e.message, style: msgStyle),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
@ -274,17 +289,18 @@ class _LogViewerWidgetState extends State<LogViewerWidget> {
|
||||||
|
|
||||||
class _LogEntry {
|
class _LogEntry {
|
||||||
_LogEntry({
|
_LogEntry({
|
||||||
|
required this.timestamp,
|
||||||
|
required this.level,
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.line,
|
required this.line,
|
||||||
required this.fileName,
|
required this.fileName,
|
||||||
this.timestamp,
|
required this.isBackground,
|
||||||
this.level,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Minimal parser based on the sample log format
|
// Minimal parser based on the sample log format
|
||||||
factory _LogEntry.parse(String raw) {
|
factory _LogEntry.parse(String raw) {
|
||||||
// Example line:
|
// Example line:
|
||||||
// 2025-12-25 23:36:52 WARNING [twonly] api.service.dart:189) > websocket error: ...
|
// 2025-12-25 23:36:52 WARNING [f] [twonly] api.service.dart:189) > websocket error: ...
|
||||||
final trimmed = raw.trim();
|
final trimmed = raw.trim();
|
||||||
DateTime? ts;
|
DateTime? ts;
|
||||||
String? level;
|
String? level;
|
||||||
|
|
@ -318,6 +334,8 @@ class _LogEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isBackground = msg.contains('[b] ');
|
||||||
|
|
||||||
msg = msg
|
msg = msg
|
||||||
.trim()
|
.trim()
|
||||||
.replaceAll('[twonly] ', '')
|
.replaceAll('[twonly] ', '')
|
||||||
|
|
@ -326,8 +344,7 @@ class _LogEntry {
|
||||||
|
|
||||||
final fileNameS = msg.split(' > ');
|
final fileNameS = msg.split(' > ');
|
||||||
final fileName = fileNameS[0];
|
final fileName = fileNameS[0];
|
||||||
|
msg = fileNameS.sublist(1).join(' > ');
|
||||||
msg = fileNameS.sublist(1).join();
|
|
||||||
|
|
||||||
return _LogEntry(
|
return _LogEntry(
|
||||||
timestamp: ts,
|
timestamp: ts,
|
||||||
|
|
@ -335,11 +352,14 @@ class _LogEntry {
|
||||||
message: msg,
|
message: msg,
|
||||||
line: raw,
|
line: raw,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
|
isBackground: isBackground,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final DateTime? timestamp;
|
final DateTime? timestamp;
|
||||||
final String? level;
|
final String? level;
|
||||||
final String message;
|
final String message;
|
||||||
final String line;
|
final String line;
|
||||||
final String fileName;
|
final String fileName;
|
||||||
|
final bool isBackground;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 0.2.2+111
|
version: 0.2.3+112
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.0
|
sdk: ^3.11.0
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ impl UserDiscoveryStore for UserDiscoveryStoreFlutter {
|
||||||
return Err(UserDiscoveryError::NotInitialized);
|
return Err(UserDiscoveryError::NotInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("Loading Config from {}", config_path.display());
|
|
||||||
Ok(std::fs::read_to_string(&config_path)?)
|
Ok(std::fs::read_to_string(&config_path)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue