mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-07-18 01:14:07 +00:00
fix errors found with glitchtip
This commit is contained in:
parent
e8829ed2c9
commit
7066e8c799
17 changed files with 275 additions and 118 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
if (e is CameraException) {
|
||||
Log.warn('Camera initialization failed (CameraException): $e');
|
||||
} else {
|
||||
Log.error('Error initializing camera: $e');
|
||||
final controllerToDispose = cameraController;
|
||||
}
|
||||
if (!assignedToGlobal) {
|
||||
unawaited(controller.dispose());
|
||||
} else {
|
||||
if (cameraController == controller) {
|
||||
cameraController = null;
|
||||
if (controllerToDispose != null) {
|
||||
unawaited(controllerToDispose.dispose());
|
||||
}
|
||||
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) {
|
||||
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) {
|
||||
if (e is CameraException) {
|
||||
Log.warn('Camera post-initialization failed (CameraException): $e');
|
||||
} else {
|
||||
Log.error('Error post-initializing camera: $e');
|
||||
final controllerToDispose = cameraController;
|
||||
cameraController = null;
|
||||
if (controllerToDispose != null) {
|
||||
unawaited(controllerToDispose.dispose());
|
||||
}
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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,18 +57,19 @@ 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(
|
||||
final lastMsgStream = await twonlyDB.messagesDao.watchLastMessage(
|
||||
widget.group.groupId,
|
||||
)).listen((update) {
|
||||
);
|
||||
if (!mounted) return;
|
||||
_lastMessageStream = lastMsgStream.listen((update) {
|
||||
protectUpdateState.protect(() async {
|
||||
await updateState(update, _messagesNotOpened);
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue