fix errors found with glitchtip

This commit is contained in:
otsmr 2026-06-20 13:11:48 +02:00
parent e8829ed2c9
commit 7066e8c799
17 changed files with 275 additions and 118 deletions

View file

@ -314,9 +314,11 @@ class UserDiscoveryCallbacks {
static Future<Uint8List?> getContactPromotion(int contactId) async {
try {
final row = await (twonlyDB.select(
twonlyDB.userDiscoveryOwnPromotions,
)..where((tbl) => tbl.contactId.equals(contactId))).getSingleOrNull();
final query = twonlyDB.select(twonlyDB.userDiscoveryOwnPromotions)
..where((tbl) => tbl.contactId.equals(contactId))
..orderBy([(tbl) => OrderingTerm.desc(tbl.versionId)])
..limit(1);
final row = await query.getSingleOrNull();
return row?.promotion;
} catch (e) {
Log.error(e);

View file

@ -128,7 +128,7 @@ class GroupsDao extends DatabaseAccessor<TwonlyDB> with _$GroupsDaoMixin {
final result = await _insertGroup(insertGroup);
if (result != null) {
await into(groupMembers).insert(
await into(groupMembers).insertOnConflictUpdate(
GroupMembersCompanion(
groupId: Value(result.groupId),
contactId: Value(

View file

@ -73,8 +73,7 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
mediaFiles,
mediaFiles.mediaId.equalsExp(messages.mediaId),
),
])
..where(
])..where(
messages.openedAt.isNull() &
messages.mediaId.isNotNull() &
messages.type.equals(MessageType.media.name) &
@ -291,11 +290,26 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
List<String> messageIds,
DateTime timestamp,
) async {
if (contactId.present) {
final contactExists = await twonlyDB.contactsDao.getContactById(contactId.value);
if (contactExists == null) {
Log.info(
'handleMessagesOpened: Contact ${contactId.value} does not exist in database, ignoring messages opened action.',
);
return;
}
}
for (final messageId in messageIds) {
try {
var actionTimestamp = timestamp;
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg != null && actionTimestamp.isBefore(msg.createdAt)) {
if (msg == null) {
Log.info(
'handleMessagesOpened: Message $messageId does not exist in database, skipping.',
);
continue;
}
if (actionTimestamp.isBefore(msg.createdAt)) {
Log.warn(
'Receiver clock skew detected for message $messageId. '
'Action timestamp $actionTimestamp is before message creation ${msg.createdAt}. '
@ -348,7 +362,11 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
'handleMessagesOpened completed for message $messageId',
);
} catch (e) {
Log.error('handleMessagesOpened failed for $messageId: $e');
Log.warn('handleMessagesOpened failed for $messageId: $e');
Log.error(
'handleMessagesOpened failed for: $e',
onlyIfSentryEnabled: true,
);
}
}
}
@ -358,6 +376,20 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
String messageId,
DateTime timestamp,
) async {
final contactExists = await twonlyDB.contactsDao.getContactById(contactId);
if (contactExists == null) {
Log.info(
'handleMessageAckByServer: Contact $contactId does not exist in database, ignoring message ack.',
);
return;
}
final msg = await getMessageById(messageId).getSingleOrNull();
if (msg == null) {
Log.info(
'handleMessageAckByServer: Message $messageId does not exist in database, skipping.',
);
return;
}
await transaction(() async {
await into(messageActions).insertOnConflictUpdate(
MessageActionsCompanion(

View file

@ -42,7 +42,9 @@ class SignalPreKeyStore extends PreKeyStore {
);
try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
await twonlyDB
.into(twonlyDB.signalPreKeyStores)
.insert(preKeyCompanion, mode: InsertMode.insertOrReplace);
} catch (e) {
Log.error('$e');
}

View file

@ -439,6 +439,10 @@ Future<void> _startBackgroundMediaUploadInternal(
// Refresh the media file state inside the mutex
await mediaService.updateFromDB();
if (mediaService.mediaFile.uploadState == UploadState.uploading) {
await _checkAndRecoverMissingUploadRequest(mediaService);
}
if (mediaService.mediaFile.uploadState == UploadState.initialized ||
mediaService.mediaFile.uploadState == UploadState.preprocessing) {
Log.info(
@ -672,7 +676,31 @@ Future<void> _createUploadRequest(MediaFileService media) async {
await media.uploadRequestPath.writeAsBytes(uploadRequestBytes);
}
Future<bool> _checkAndRecoverMissingUploadRequest(
MediaFileService media, {
bool triggerBackgroundUpload = false,
}) async {
if (!media.uploadRequestPath.existsSync()) {
Log.warn(
'UploadRequestPath for media ${media.mediaFile.mediaId} does not exist. Reverting to preprocessing.',
);
await media.setUploadState(UploadState.preprocessing);
if (triggerBackgroundUpload) {
unawaited(startBackgroundMediaUpload(media));
}
return true;
}
return false;
}
Future<void> _uploadUploadRequest(MediaFileService media) async {
if (await _checkAndRecoverMissingUploadRequest(
media,
triggerBackgroundUpload: true,
)) {
return;
}
final currentMedia = await twonlyDB.mediaFilesDao.getMediaFileById(
media.mediaFile.mediaId,
);
@ -722,6 +750,13 @@ Future<void> uploadFileFastOrEnqueue(
UploadTask task,
MediaFileService media,
) async {
if (await _checkAndRecoverMissingUploadRequest(
media,
triggerBackgroundUpload: true,
)) {
return;
}
final requestMultipart = http.MultipartRequest(
'POST',
Uri.parse(task.url),

View file

@ -101,14 +101,18 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
if (receiptId == null && receipt == null) return null;
try {
if (receipt == null) {
// ignore: parameter_assignments
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) {
Log.warn('[$receiptId] Receipt not found.');
return null;
}
final targetReceiptId = receipt?.receiptId ?? receiptId!;
final loadedReceipt = await twonlyDB.receiptsDao.getReceiptById(
targetReceiptId,
);
if (loadedReceipt == null) {
Log.info(
'[$targetReceiptId] Receipt not found (might have been processed or deleted).',
);
return null;
}
// ignore: parameter_assignments
receipt = loadedReceipt;
if (receipt.retryCount >= 2) {
// After two retries, change the receiptId. This addresses a bug where the receiver received the message and marked it as received,
@ -138,7 +142,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({
if (!onlyReturnEncryptedData &&
receipt.ackByServerAt != null &&
receipt.markForRetry == null) {
Log.error('Message already uploaded and mark for retry is not set!');
Log.info('Message already uploaded and mark for retry is not set.');
return null;
}

View file

@ -33,17 +33,18 @@ Future<void> compressImage(
Log.info('Compressed images size in bytes: ${compressedBytes.length}');
if (compressedBytes.length >= 1 * 1000 * 1000) {
if (compressedBytes.length >= 2 * 1000 * 1000) {
// if the media file is over 1MB compress it with 60%
final tmpCompressedBytes = await FlutterImageCompress.compressWithFile(
sourceFile.path,
format: CompressFormat.webp,
quality: 60,
);
if (tmpCompressedBytes != null) {
if (tmpCompressedBytes == null) {
Log.error(
'Could not compress media file with 60%: $sourceFile. Sending original 90% compressed file.',
);
} else {
compressedBytes = tmpCompressedBytes;
}
}
@ -106,11 +107,11 @@ Future<void> compressAndOverlayVideo(MediaFileService media) async {
},
);
} catch (e) {
Log.error('during video compression: $e');
Log.warn('during video compression: $e');
}
if (compressedPath == null) {
Log.error('Could not compress video using original video.');
Log.warn('Could not compress video using original video.');
// as a fall back use the non compressed version
media.ffmpegOutputPath.copySync(media.tempPath.path);
}

View file

@ -252,7 +252,7 @@ class MediaFileService {
Future<void> compressMedia() async {
if (!originalPath.existsSync()) {
Log.error('Could not compress as original media does not exists.');
Log.warn('Could not compress as original media does not exists.');
return;
}
@ -296,7 +296,8 @@ class MediaFileService {
(thumbnailPath.existsSync() && thumbnailPath.lengthSync() > 0) ||
mediaFile.type == MediaType.audio ||
((mediaFile.type == MediaType.image || mediaFile.type == MediaType.gif) &&
storedPath.existsSync() && storedPath.lengthSync() > 0);
storedPath.existsSync() &&
storedPath.lengthSync() > 0);
Future<void> storeMediaFile() async {
Log.info('Storing media file ${mediaFile.mediaId}');
@ -327,8 +328,8 @@ class MediaFileService {
}
}
} else {
Log.error(
'Could not store image neither as ${tempPath.path} does not exists.',
Log.warn(
'Could not store image locally as ${tempPath.path} does not exist.',
);
}
unawaited(createThumbnail());
@ -469,7 +470,7 @@ class MediaFileService {
format: CompressFormat.webp,
quality: 90,
);
if (webpBytes.isNotEmpty) {
await storedPath.writeAsBytes(webpBytes);
} else {
@ -503,7 +504,7 @@ class MediaFileService {
);
await updateFromDB();
} catch (e) {
Log.error(
Log.warn(
'Error auto-cropping transparent borders for mediaId ${mediaFile.mediaId}: $e',
);
await twonlyDB.mediaFilesDao.updateMedia(

View file

@ -20,6 +20,7 @@ class GroupContextMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
final navigator = Navigator.of(context);
return ContextMenu(
items: [
if (!group.archived)
@ -27,9 +28,7 @@ class GroupContextMenu extends StatelessWidget {
title: context.lang.contextMenuArchiveUser,
onTap: () async {
const update = GroupsCompanion(archived: Value(true));
if (context.mounted) {
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
}
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
},
icon: Icons.archive_outlined,
),
@ -38,15 +37,13 @@ class GroupContextMenu extends StatelessWidget {
title: context.lang.contextMenuUndoArchiveUser,
onTap: () async {
const update = GroupsCompanion(archived: Value(false));
if (context.mounted) {
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
}
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
},
icon: Icons.unarchive_outlined,
),
ContextMenuItem(
title: context.lang.contextMenuOpenChat,
onTap: () => context.push(Routes.chatsMessages(group.groupId)),
onTap: () => navigator.context.push(Routes.chatsMessages(group.groupId)),
icon: FontAwesomeIcons.comments,
),
if (!group.archived)
@ -56,9 +53,7 @@ class GroupContextMenu extends StatelessWidget {
: context.lang.contextMenuPin,
onTap: () async {
final update = GroupsCompanion(pinned: Value(!group.pinned));
if (context.mounted) {
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
}
await twonlyDB.groupsDao.updateGroup(group.groupId, update);
},
icon: group.pinned
? FontAwesomeIcons.thumbtackSlash
@ -69,9 +64,9 @@ class GroupContextMenu extends StatelessWidget {
icon: FontAwesomeIcons.trashCan,
onTap: () async {
final ok = await showAlertDialog(
context,
context.lang.deleteTitle,
context.lang.groupContextMenuDeleteGroup,
navigator.context,
navigator.context.lang.deleteTitle,
navigator.context.lang.groupContextMenuDeleteGroup,
);
if (ok) {
await twonlyDB.messagesDao.deleteMessagesByGroupId(group.groupId);

View file

@ -17,12 +17,13 @@ class UserContextMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
final navigator = Navigator.of(context);
return ContextMenu(
minWidth: 150,
items: [
ContextMenuItem(
title: context.lang.contextMenuUserProfile,
onTap: () => context.push(Routes.profileContact(contact.userId)),
onTap: () => navigator.context.push(Routes.profileContact(contact.userId)),
icon: FontAwesomeIcons.user,
),
],

View file

@ -178,7 +178,8 @@ class MainCameraController {
selectedCameraDetails.isZoomAble = false;
if (cameraController == null || !cameraController!.value.isInitialized) {
final currentController = cameraController;
if (currentController == null || !currentController.value.isInitialized) {
final controllerToDispose = cameraController;
cameraController = null;
if (controllerToDispose != null) {
@ -188,7 +189,7 @@ class MainCameraController {
final hasMic = await micPermissionFuture;
if (sessionId != _cameraSessionId) return;
cameraController = CameraController(
final controller = CameraController(
AppEnvironment.cameras[cameraId],
ResolutionPreset.high,
enableAudio: hasMic,
@ -196,22 +197,60 @@ class MainCameraController {
? ImageFormatGroup.nv21
: ImageFormatGroup.bgra8888,
);
var assignedToGlobal = false;
try {
_initializeFuture = cameraController?.initialize();
_initializeFuture = controller.initialize();
await _initializeFuture;
await cameraController!.startImageStream(_processCameraImage);
await cameraController!.setZoomLevel(selectedCameraDetails.scaleFactor);
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
await cameraController!.setVideoStabilizationMode(
VideoStabilizationMode.level1,
if (sessionId != _cameraSessionId) {
unawaited(controller.dispose());
return;
}
if (!controller.value.isInitialized) {
throw CameraException(
'Uninitialized CameraController',
'CameraController was not initialized after initialize() completed.',
);
}
await controller.startImageStream(_processCameraImage);
if (sessionId != _cameraSessionId) {
unawaited(controller.dispose());
return;
}
await controller.setZoomLevel(selectedCameraDetails.scaleFactor);
if (sessionId != _cameraSessionId) {
unawaited(controller.dispose());
return;
}
if (userService.currentUser.videoStabilizationEnabled && !kDebugMode) {
await controller.setVideoStabilizationMode(
VideoStabilizationMode.level1,
);
if (sessionId != _cameraSessionId) {
unawaited(controller.dispose());
return;
}
}
cameraController = controller;
assignedToGlobal = true;
} catch (e) {
Log.error('Error initializing camera: $e');
final controllerToDispose = cameraController;
cameraController = null;
if (controllerToDispose != null) {
unawaited(controllerToDispose.dispose());
if (e is CameraException) {
Log.warn('Camera initialization failed (CameraException): $e');
} else {
Log.error('Error initializing camera: $e');
}
if (!assignedToGlobal) {
unawaited(controller.dispose());
} else {
if (cameraController == controller) {
cameraController = null;
}
unawaited(controller.dispose());
}
initCameraStarted = false;
return;
@ -219,35 +258,64 @@ class MainCameraController {
} else {
try {
if (!isVideoRecording) {
await cameraController!.stopImageStream();
try {
await currentController.stopImageStream();
} catch (e) {
Log.warn('Could not stop image stream: $e');
}
}
if (sessionId != _cameraSessionId) return;
selectedCameraDetails.scaleFactor = 1;
await cameraController!.setZoomLevel(1);
await cameraController!.setDescription(
await currentController.setZoomLevel(1);
if (sessionId != _cameraSessionId) return;
await currentController.setDescription(
AppEnvironment.cameras[cameraId],
);
if (sessionId != _cameraSessionId) return;
if (!isVideoRecording) {
await cameraController!.startImageStream(_processCameraImage);
await currentController.startImageStream(_processCameraImage);
}
} catch (e) {
Log.error('Error switching camera description: $e');
if (e is CameraException) {
Log.warn('Camera description switch failed (CameraException): $e');
} else {
Log.error('Error switching camera description: $e');
}
initCameraStarted = false;
return;
}
}
if (sessionId != _cameraSessionId) return;
final controller = cameraController;
if (controller == null) {
initCameraStarted = false;
return;
}
try {
await cameraController!.lockCaptureOrientation(
await controller.lockCaptureOrientation(
DeviceOrientation.portraitUp,
);
await cameraController!.setFlashMode(
if (sessionId != _cameraSessionId) return;
await controller.setFlashMode(
selectedCameraDetails.isFlashOn ? FlashMode.always : FlashMode.off,
);
selectedCameraDetails.maxAvailableZoom = await cameraController!
if (sessionId != _cameraSessionId) return;
selectedCameraDetails.maxAvailableZoom = await controller
.getMaxZoomLevel();
selectedCameraDetails.minAvailableZoom = await cameraController!
if (sessionId != _cameraSessionId) return;
selectedCameraDetails.minAvailableZoom = await controller
.getMinZoomLevel();
if (sessionId != _cameraSessionId) return;
selectedCameraDetails
..isZoomAble =
selectedCameraDetails.maxAvailableZoom !=
@ -263,12 +331,15 @@ class MainCameraController {
initCameraStarted = false;
setState?.call();
} catch (e) {
Log.error('Error post-initializing camera: $e');
final controllerToDispose = cameraController;
cameraController = null;
if (controllerToDispose != null) {
unawaited(controllerToDispose.dispose());
if (e is CameraException) {
Log.warn('Camera post-initialization failed (CameraException): $e');
} else {
Log.error('Error post-initializing camera: $e');
}
if (cameraController == controller) {
cameraController = null;
}
unawaited(controller.dispose());
initCameraStarted = false;
return;
}
@ -348,8 +419,9 @@ class MainCameraController {
}
InputImage? _inputImageFromCameraImage(CameraImage image) {
if (cameraController == null) return null;
final camera = cameraController!.description;
final controller = cameraController;
if (controller == null) return null;
final camera = controller.description;
final sensorOrientation = camera.sensorOrientation;
InputImageRotation? rotation;
@ -358,7 +430,7 @@ class MainCameraController {
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
} else if (Platform.isAndroid) {
var rotationCompensation =
_orientations[cameraController!.value.deviceOrientation];
_orientations[controller.value.deviceOrientation];
if (rotationCompensation == null) return null;
if (camera.lensDirection == CameraLensDirection.front) {
// front-facing
@ -408,14 +480,15 @@ class MainCameraController {
if (_isBusy) return;
_isBusy = true;
final barcodes = await _barcodeScanner.processImage(inputImage);
final controller = cameraController;
if (inputImage.metadata?.size != null &&
inputImage.metadata?.rotation != null &&
cameraController != null) {
controller != null) {
final painter = BarcodeDetectorPainter(
barcodes,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
controller.description.lensDirection,
);
qrCodePain = CustomPaint(painter: painter);
@ -501,9 +574,10 @@ class MainCameraController {
if (_isBusyFaces) return;
_isBusyFaces = true;
final faces = await _faceDetector.processImage(inputImage);
final controller = cameraController;
if (inputImage.metadata?.size != null &&
inputImage.metadata?.rotation != null &&
cameraController != null) {
controller != null) {
if (faces.isNotEmpty) {
CustomPainter? painter;
switch (_currentFilterType) {
@ -512,7 +586,7 @@ class MainCameraController {
faces,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
controller.description.lensDirection,
);
case FaceFilterType.beardUpperLipGreen:
painter = BeardFilterPainter(
@ -520,7 +594,7 @@ class MainCameraController {
faces,
inputImage.metadata!.size,
inputImage.metadata!.rotation,
cameraController!.description.lensDirection,
controller.description.lensDirection,
);
case FaceFilterType.none:
break;

View file

@ -36,13 +36,13 @@ class _UserListItem extends State<GroupListItemComp> {
Message? _currentMessage;
List<Message> _messagesNotOpened = [];
late StreamSubscription<List<Message>> _messagesNotOpenedStream;
StreamSubscription<List<Message>>? _messagesNotOpenedStream;
Message? _lastMessage;
Reaction? _lastReaction;
late StreamSubscription<Message?> _lastMessageStream;
late StreamSubscription<Reaction?> _lastReactionStream;
late StreamSubscription<List<MediaFile>> _lastMediaFilesStream;
StreamSubscription<Message?>? _lastMessageStream;
StreamSubscription<Reaction?>? _lastReactionStream;
StreamSubscription<List<MediaFile>>? _lastMediaFilesStream;
List<Message> _previewMessages = [];
final List<MediaFile> _previewMediaFiles = [];
@ -57,22 +57,23 @@ class _UserListItem extends State<GroupListItemComp> {
@override
void dispose() {
_messagesNotOpenedStream.cancel();
_lastReactionStream.cancel();
_lastMessageStream.cancel();
_lastMediaFilesStream.cancel();
_messagesNotOpenedStream?.cancel();
_lastReactionStream?.cancel();
_lastMessageStream?.cancel();
_lastMediaFilesStream?.cancel();
super.dispose();
}
Future<void> initStreams() async {
_lastMessageStream =
(await twonlyDB.messagesDao.watchLastMessage(
widget.group.groupId,
)).listen((update) {
protectUpdateState.protect(() async {
await updateState(update, _messagesNotOpened);
});
});
final lastMsgStream = await twonlyDB.messagesDao.watchLastMessage(
widget.group.groupId,
);
if (!mounted) return;
_lastMessageStream = lastMsgStream.listen((update) {
protectUpdateState.protect(() async {
await updateState(update, _messagesNotOpened);
});
});
_lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) {
if (!mounted) return;
@ -103,6 +104,7 @@ class _UserListItem extends State<GroupListItemComp> {
final groupContacts = await twonlyDB.groupsDao.getGroupContact(
widget.group.groupId,
);
if (!mounted) return;
if (groupContacts.length == 1) {
_receiverDeletedAccount = groupContacts.first.accountDeleted;
}

View file

@ -99,6 +99,7 @@ class MessageContextMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
final navigator = Navigator.of(context);
return ContextMenu(
items: [
if (!message.isDeletedFromSender)
@ -107,7 +108,7 @@ class MessageContextMenu extends StatelessWidget {
onTap: () async {
final layer =
await showModalBottomSheet(
context: context,
context: navigator.context,
backgroundColor: Colors.black,
builder: (context) {
return const EmojiPickerBottom();
@ -138,7 +139,7 @@ class MessageContextMenu extends StatelessWidget {
if (mediaFileService?.canBeOpenedAgain ?? false)
ContextMenuItem(
title: context.lang.contextMenuViewAgain,
onTap: () => reopenMediaFile(context),
onTap: () => reopenMediaFile(navigator.context),
icon: FontAwesomeIcons.clockRotateLeft,
),
if (!message.isDeletedFromSender)
@ -155,7 +156,7 @@ class MessageContextMenu extends StatelessWidget {
ContextMenuItem(
title: context.lang.edit,
onTap: () async {
await editTextMessage(context, message);
await editTextMessage(navigator.context, message);
},
icon: FontAwesomeIcons.pencil,
),
@ -172,13 +173,13 @@ class MessageContextMenu extends StatelessWidget {
title: context.lang.delete,
onTap: () async {
final delete = await showAlertDialog(
context,
context.lang.deleteTitle,
navigator.context,
navigator.context.lang.deleteTitle,
null,
customOk:
(message.senderId == null && !message.isDeletedFromSender)
? context.lang.deleteOkBtnForAll
: context.lang.deleteOkBtnForMe,
? navigator.context.lang.deleteOkBtnForAll
: navigator.context.lang.deleteOkBtnForMe,
);
if (delete) {
if (message.senderId == null && !message.isDeletedFromSender) {
@ -209,8 +210,7 @@ class MessageContextMenu extends StatelessWidget {
ContextMenuItem(
title: context.lang.info,
onTap: () async {
await Navigator.push(
context,
await navigator.push(
MaterialPageRoute(
builder: (context) {
return MessageInfoView(

View file

@ -137,6 +137,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final Mutex _messageUpdateLock = Mutex();
bool _isViewActive() {
if (!mounted) return false;
return !AppState.isAppInBackground &&
(ModalRoute.of(context)?.isCurrent ?? false);
}
@ -358,7 +359,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (!mounted) return;
if (!currentMediaLocal.tempPath.existsSync()) {
Log.error('Temp media file not found...');
Log.warn('Temp media file not found for media ID: ${currentMediaLocal.mediaFile.mediaId}');
await handleMediaError(currentMediaLocal.mediaFile);
return advanceToNextMediaOrExit();
}
@ -468,7 +469,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
..play();
})
.catchError((Object err, StackTrace st) {
Log.error('Video player initialization error', err, st);
Log.error('Video player initialization error', error: err, stackTrace: st);
return null;
});
}
@ -511,12 +512,16 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}
Future<void> onPressedSaveToGallery() async {
final media = currentMedia;
final msg = currentMessage;
if (media == null || msg == null) return;
setState(() {
imageSaving = true;
});
await currentMedia!.storeMediaFile();
await media.storeMediaFile();
await twonlyDB.messagesDao.updateMessageId(
currentMessage!.messageId,
msg.messageId,
const MessagesCompanion(
mediaStored: Value(true),
),
@ -526,7 +531,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
pb.EncryptedContent(
mediaUpdate: pb.EncryptedContent_MediaUpdate(
type: pb.EncryptedContent_MediaUpdate_Type.STORED,
targetMessageId: currentMessage!.messageId,
targetMessageId: msg.messageId,
),
),
);
@ -553,11 +558,12 @@ class _MediaViewerViewState extends State<MediaViewerView> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (currentMedia != null &&
currentMessage != null &&
!currentMedia!.mediaFile.requiresAuthentication &&
currentMedia!.mediaFile.displayLimitInMilliseconds == null)
MyIconButton(
variant: MyIconButtonVariant.secondary,
onPressed: (currentMedia == null) ? null : onPressedSaveToGallery,
onPressed: (currentMedia == null || currentMessage == null) ? null : onPressedSaveToGallery,
icon: imageSaving
? const SizedBox(
width: 16,

View file

@ -118,6 +118,7 @@ class GroupMemberContextMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
final navigator = Navigator.of(context);
return ContextMenu(
items: [
if (contact.accepted)
@ -131,15 +132,15 @@ class GroupMemberContextMenu extends StatelessWidget {
// create
return;
}
if (!context.mounted) return;
await context.push(Routes.chatsMessages(directChat.groupId));
if (!navigator.mounted) return;
await navigator.context.push(Routes.chatsMessages(directChat.groupId));
},
icon: FontAwesomeIcons.message,
),
if (!contact.accepted)
ContextMenuItem(
title: context.lang.createContactRequest,
onTap: () => _makeContactRequest(context),
onTap: () => _makeContactRequest(navigator.context),
icon: FontAwesomeIcons.userPlus,
),
if (member.groupPublicKey != null &&
@ -147,7 +148,7 @@ class GroupMemberContextMenu extends StatelessWidget {
member.memberState == MemberState.normal)
ContextMenuItem(
title: context.lang.makeAdmin,
onTap: () => _makeContactAdmin(context),
onTap: () => _makeContactAdmin(navigator.context),
icon: FontAwesomeIcons.key,
),
if (member.groupPublicKey != null &&
@ -155,13 +156,13 @@ class GroupMemberContextMenu extends StatelessWidget {
member.memberState == MemberState.admin)
ContextMenuItem(
title: context.lang.removeAdmin,
onTap: () => _removeContactAsAdmin(context),
onTap: () => _removeContactAsAdmin(navigator.context),
icon: FontAwesomeIcons.key,
),
if (group.isGroupAdmin && member.groupPublicKey != null)
ContextMenuItem(
title: context.lang.removeFromGroup,
onTap: () => _removeContactFromGroup(context),
onTap: () => _removeContactFromGroup(navigator.context),
icon: FontAwesomeIcons.rightFromBracket,
),
],

View file

@ -57,7 +57,7 @@ impl BackupArchive {
if is_db {
// To avoid write-lock conflicts with Dart (which has the live database open in write mode),
// we copy the database file first, then open the copy in write mode to perform the backup.
// we copy the database file first, then open the copy to perform the backup.
let temp_copy_path = backup_data_dir.join(format!("{}.temp_copy", file_name));
std::fs::copy(&file_path, &temp_copy_path)?;
@ -72,13 +72,14 @@ impl BackupArchive {
.await?;
// Close database connection to release file lock before removing it
drop(db);
db.pool.close().await;
remove_file(&temp_copy_path)?;
// Perform integrity check of the new database file
let backup_db =
Database::new(&backup_database_file, encryption_key.as_deref(), false).await?;
Database::new(&backup_database_file, encryption_key.as_deref(), true).await?;
backup_db.check_integrity().await?;
backup_db.pool.close().await;
} else {
let file_backup = backup_data_dir.join(file_name);
std::fs::copy(file_path, file_backup)?;

View file

@ -29,7 +29,7 @@ impl Database {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Delete)
.foreign_keys(true)
.read_only(read_only)
.busy_timeout(Duration::from_millis(5000))
.busy_timeout(Duration::from_secs(30))
.pragma("synchronous", "FULL")
.pragma("recursive_triggers", "ON")
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
@ -39,7 +39,7 @@ impl Database {
}
let pool = SqlitePoolOptions::new()
.acquire_timeout(Duration::from_secs(5))
.acquire_timeout(Duration::from_secs(30))
.max_connections(10)
.connect_with(connect_options)
.await?;