mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-06-02 20:42:12 +00:00
fixes database issues
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
This commit is contained in:
parent
dc0ef25d73
commit
358f93979e
12 changed files with 300 additions and 169 deletions
|
|
@ -1,5 +1,12 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.2.25
|
||||||
|
|
||||||
|
- Improves: Smaller UI changes
|
||||||
|
- Fix: Migration issue that resulted in a corrupted backup mechanism
|
||||||
|
- Fix: Database issues causing messages to be lost or the database to be corrupted
|
||||||
|
- Fix: Permission view did not disappear after they were granted
|
||||||
|
|
||||||
## 0.2.23
|
## 0.2.23
|
||||||
|
|
||||||
- Improves: Smaller UI changes
|
- Improves: Smaller UI changes
|
||||||
|
|
|
||||||
|
|
@ -92,14 +92,15 @@ class TwonlyDB extends _$TwonlyDB {
|
||||||
shareAcrossIsolates: true,
|
shareAcrossIsolates: true,
|
||||||
setup: (rawDb) {
|
setup: (rawDb) {
|
||||||
rawDb
|
rawDb
|
||||||
..execute('PRAGMA journal_mode=WAL;')
|
..execute('PRAGMA journal_mode=DELETE;')
|
||||||
..execute('PRAGMA synchronous=FULL;')
|
..execute('PRAGMA synchronous=FULL;')
|
||||||
..execute('PRAGMA busy_timeout=5000;');
|
..execute('PRAGMA busy_timeout=5000;');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
if (userService.isUserCreated && userService.currentUser.enableDatabaseLogging) {
|
if (userService.isUserCreated &&
|
||||||
|
userService.currentUser.enableDatabaseLogging) {
|
||||||
return connection.interceptWith(DriftLoggingInterceptor());
|
return connection.interceptWith(DriftLoggingInterceptor());
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ class UserService {
|
||||||
if (userDataMap != null) {
|
if (userDataMap != null) {
|
||||||
final userData = UserData.fromJson(userDataMap);
|
final userData = UserData.fromJson(userDataMap);
|
||||||
await RustKeyManager.setUserId(userId: userData.userId);
|
await RustKeyManager.setUserId(userId: userData.userId);
|
||||||
|
try {
|
||||||
|
// Ensure that the old userData is removed as it breaks the backup mechanism.
|
||||||
|
// This code can be removed when all users have updated to the latest version...
|
||||||
|
await SecureStorage.instance.delete(key: 'userData');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not delete user data from SecureStorage: $e');
|
||||||
|
}
|
||||||
return userData;
|
return userData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,15 +65,20 @@ class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> _migrateFromSecureStorage(UserData userData) async {
|
static Future<void> _migrateFromSecureStorage(UserData userData) async {
|
||||||
// Currently empty migration logic as requested, but we MUST store the data
|
|
||||||
await KeyValueStore.put('user', userData.toJson());
|
await KeyValueStore.put('user', userData.toJson());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await RustKeyManager.setUserId(userId: userData.userId);
|
await RustKeyManager.setUserId(userId: userData.userId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error('Could not set userId in RustKeyManager during migration: $e');
|
Log.error('Could not set userId in RustKeyManager during migration: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Log migration
|
try {
|
||||||
|
await SecureStorage.instance.delete(key: 'userData');
|
||||||
|
} catch (e) {
|
||||||
|
Log.error('Could not delete user data from SecureStorage: $e');
|
||||||
|
}
|
||||||
|
|
||||||
Log.info('Migrated user data from SecureStorage to KeyValueStore');
|
Log.info('Migrated user data from SecureStorage to KeyValueStore');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,23 +63,25 @@ class CameraPreviewControllerView extends StatefulWidget {
|
||||||
final bool hideControllers;
|
final bool hideControllers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<CameraPreviewControllerView> createState() => _CameraPreviewControllerViewState();
|
State<CameraPreviewControllerView> createState() =>
|
||||||
|
_CameraPreviewControllerViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CameraPreviewControllerViewState extends State<CameraPreviewControllerView> {
|
class _CameraPreviewControllerViewState
|
||||||
|
extends State<CameraPreviewControllerView> {
|
||||||
Future<bool>? _permissionsFuture;
|
Future<bool>? _permissionsFuture;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (!AppState.hasCameraPermissions) {
|
_permissionsFuture = checkPermissions().then((hasPermission) {
|
||||||
_permissionsFuture = checkPermissions().then((hasPermission) {
|
if (hasPermission && mounted) {
|
||||||
if (hasPermission) {
|
setState(() {
|
||||||
AppState.hasCameraPermissions = true;
|
AppState.hasCameraPermissions = true;
|
||||||
}
|
});
|
||||||
return hasPermission;
|
}
|
||||||
});
|
return hasPermission;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -107,6 +109,10 @@ class _CameraPreviewControllerViewState extends State<CameraPreviewControllerVie
|
||||||
} else {
|
} else {
|
||||||
return PermissionHandlerView(
|
return PermissionHandlerView(
|
||||||
onSuccess: () {
|
onSuccess: () {
|
||||||
|
setState(() {
|
||||||
|
AppState.hasCameraPermissions = true;
|
||||||
|
_permissionsFuture = Future.value(true);
|
||||||
|
});
|
||||||
widget.mainController.selectCamera(0, true);
|
widget.mainController.selectCamera(0, true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -241,7 +247,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
_hasAudioPermission = await Permission.microphone.isGranted;
|
_hasAudioPermission = await Permission.microphone.isGranted;
|
||||||
|
|
||||||
if (!_hasAudioPermission && !userService.currentUser.requestedAudioPermission) {
|
if (!_hasAudioPermission &&
|
||||||
|
!userService.currentUser.requestedAudioPermission) {
|
||||||
await UserService.update((u) => u.requestedAudioPermission = true);
|
await UserService.update((u) => u.requestedAudioPermission = true);
|
||||||
await requestMicrophonePermission();
|
await requestMicrophonePermission();
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +269,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateScaleFactor(double newScale) async {
|
Future<void> updateScaleFactor(double newScale) async {
|
||||||
if (mc.selectedCameraDetails.scaleFactor == newScale || mc.cameraController == null) {
|
if (mc.selectedCameraDetails.scaleFactor == newScale ||
|
||||||
|
mc.cameraController == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await mc.cameraController?.setZoomLevel(
|
await mc.cameraController?.setZoomLevel(
|
||||||
|
|
@ -345,7 +353,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
bool sharedFromGallery = false,
|
bool sharedFromGallery = false,
|
||||||
MediaType? mediaType,
|
MediaType? mediaType,
|
||||||
}) async {
|
}) async {
|
||||||
final type = mediaType ?? ((videoFilePath != null) ? MediaType.video : MediaType.image);
|
final type =
|
||||||
|
mediaType ??
|
||||||
|
((videoFilePath != null) ? MediaType.video : MediaType.image);
|
||||||
final mediaFileService = await initializeMediaUpload(
|
final mediaFileService = await initializeMediaUpload(
|
||||||
type,
|
type,
|
||||||
userService.currentUser.defaultShowTime,
|
userService.currentUser.defaultShowTime,
|
||||||
|
|
@ -386,9 +396,10 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
mainCameraController: mc,
|
mainCameraController: mc,
|
||||||
previewLink: mc.sharedLinkForPreview,
|
previewLink: mc.sharedLinkForPreview,
|
||||||
),
|
),
|
||||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
transitionsBuilder:
|
||||||
return child;
|
(context, animation, secondaryAnimation, child) {
|
||||||
},
|
return child;
|
||||||
|
},
|
||||||
transitionDuration: Duration.zero,
|
transitionDuration: Duration.zero,
|
||||||
reverseTransitionDuration: Duration.zero,
|
reverseTransitionDuration: Duration.zero,
|
||||||
),
|
),
|
||||||
|
|
@ -418,13 +429,16 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isFront => mc.cameraController?.description.lensDirection == CameraLensDirection.front;
|
bool get isFront =>
|
||||||
|
mc.cameraController?.description.lensDirection ==
|
||||||
|
CameraLensDirection.front;
|
||||||
|
|
||||||
Future<void> onPanUpdate(dynamic details) async {
|
Future<void> onPanUpdate(dynamic details) async {
|
||||||
if (details == null) {
|
if (details == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mc.cameraController == null || !mc.cameraController!.value.isInitialized) {
|
if (mc.cameraController == null ||
|
||||||
|
!mc.cameraController!.value.isInitialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,7 +567,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startVideoRecording() async {
|
Future<void> startVideoRecording() async {
|
||||||
if (mc.cameraController != null && mc.cameraController!.value.isRecordingVideo) {
|
if (mc.cameraController != null &&
|
||||||
|
mc.cameraController!.value.isRecordingVideo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -573,7 +588,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
_currentTime = clock.now();
|
_currentTime = clock.now();
|
||||||
});
|
});
|
||||||
if (_videoRecordingStarted != null &&
|
if (_videoRecordingStarted != null &&
|
||||||
_currentTime.difference(_videoRecordingStarted!).inSeconds >= maxVideoRecordingTime) {
|
_currentTime.difference(_videoRecordingStarted!).inSeconds >=
|
||||||
|
maxVideoRecordingTime) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
_videoRecordingTimer = null;
|
_videoRecordingTimer = null;
|
||||||
stopVideoRecording();
|
stopVideoRecording();
|
||||||
|
|
@ -610,7 +626,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
_videoRecordingLocked = false;
|
_videoRecordingLocked = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mc.cameraController == null || !mc.cameraController!.value.isRecordingVideo) {
|
if (mc.cameraController == null ||
|
||||||
|
!mc.cameraController!.value.isRecordingVideo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -638,7 +655,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length || mc.cameraController == null) {
|
if (mc.selectedCameraDetails.cameraId >= AppEnvironment.cameras.length ||
|
||||||
|
mc.cameraController == null) {
|
||||||
return Container();
|
return Container();
|
||||||
}
|
}
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
|
|
@ -662,7 +680,9 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
_baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
|
_baseScaleFactor = mc.selectedCameraDetails.scaleFactor;
|
||||||
});
|
});
|
||||||
// Get the position of the pointer
|
// Get the position of the pointer
|
||||||
final renderBox = keyTriggerButton.currentContext!.findRenderObject()! as RenderBox;
|
final renderBox =
|
||||||
|
keyTriggerButton.currentContext!.findRenderObject()!
|
||||||
|
as RenderBox;
|
||||||
final localPosition = renderBox.globalToLocal(
|
final localPosition = renderBox.globalToLocal(
|
||||||
details.globalPosition,
|
details.globalPosition,
|
||||||
);
|
);
|
||||||
|
|
@ -698,18 +718,24 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null && !mc.isVideoRecording)
|
if (!mc.isSharePreviewIsShown &&
|
||||||
|
widget.sendToGroup != null &&
|
||||||
|
!mc.isVideoRecording)
|
||||||
ShowTitleText(
|
ShowTitleText(
|
||||||
title: widget.sendToGroup!.groupName,
|
title: widget.sendToGroup!.groupName,
|
||||||
desc: context.lang.cameraPreviewSendTo,
|
desc: context.lang.cameraPreviewSendTo,
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown && mc.sharedLinkForPreview != null && !mc.isVideoRecording)
|
if (!mc.isSharePreviewIsShown &&
|
||||||
|
mc.sharedLinkForPreview != null &&
|
||||||
|
!mc.isVideoRecording)
|
||||||
ShowTitleText(
|
ShowTitleText(
|
||||||
title: mc.sharedLinkForPreview?.host ?? '',
|
title: mc.sharedLinkForPreview?.host ?? '',
|
||||||
desc: 'Link',
|
desc: 'Link',
|
||||||
isLink: true,
|
isLink: true,
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown && !mc.isVideoRecording && !widget.hideControllers)
|
if (!mc.isSharePreviewIsShown &&
|
||||||
|
!mc.isVideoRecording &&
|
||||||
|
!widget.hideControllers)
|
||||||
CameraTopActions(
|
CameraTopActions(
|
||||||
selectedCameraDetails: mc.selectedCameraDetails,
|
selectedCameraDetails: mc.selectedCameraDetails,
|
||||||
hasAudioPermission: _hasAudioPermission,
|
hasAudioPermission: _hasAudioPermission,
|
||||||
|
|
@ -753,7 +779,8 @@ class _CameraPreviewViewState extends State<CameraPreviewView> {
|
||||||
videoRecordingStarted: _videoRecordingStarted,
|
videoRecordingStarted: _videoRecordingStarted,
|
||||||
maxVideoRecordingTime: maxVideoRecordingTime,
|
maxVideoRecordingTime: maxVideoRecordingTime,
|
||||||
),
|
),
|
||||||
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null || widget.hideControllers)
|
if (!mc.isSharePreviewIsShown && widget.sendToGroup != null ||
|
||||||
|
widget.hideControllers)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 5,
|
left: 5,
|
||||||
top: 10,
|
top: 10,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// ignore_for_file: avoid_dynamic_calls
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
@ -24,31 +24,56 @@ Future<bool> checkPermissions() async {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PermissionHandlerViewState extends State<PermissionHandlerView> {
|
class PermissionHandlerViewState extends State<PermissionHandlerView>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
Timer? _timer;
|
||||||
|
bool _isSuccessTriggered = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
_timer = Timer.periodic(const Duration(milliseconds: 500), (timer) async {
|
||||||
|
await _checkAndTriggerSuccess();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
_checkAndTriggerSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkAndTriggerSuccess() async {
|
||||||
|
if (_isSuccessTriggered) return;
|
||||||
|
try {
|
||||||
|
if (await checkPermissions()) {
|
||||||
|
_isSuccessTriggered = true;
|
||||||
|
_timer?.cancel();
|
||||||
|
// ignore: avoid_dynamic_calls
|
||||||
|
widget.onSuccess();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<Permission, PermissionStatus>> permissionServices() async {
|
Future<Map<Permission, PermissionStatus>> permissionServices() async {
|
||||||
// try {
|
|
||||||
final statuses = await [
|
final statuses = await [
|
||||||
Permission.camera,
|
Permission.camera,
|
||||||
// Permission.microphone,
|
|
||||||
Permission.notification,
|
Permission.notification,
|
||||||
].request();
|
].request();
|
||||||
// } catch (e) {}
|
|
||||||
// You can request multiple permissions at once.
|
|
||||||
|
|
||||||
// if (statuses[Permission.microphone]!.isPermanentlyDenied) {
|
|
||||||
// openAppSettings();
|
|
||||||
// // setState(() {});
|
|
||||||
// } else {
|
|
||||||
// // if (statuses[Permission.microphone]!.isDenied) {
|
|
||||||
// // }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (statuses[Permission.camera]!.isPermanentlyDenied) {
|
if (statuses[Permission.camera]!.isPermanentlyDenied) {
|
||||||
await openAppSettings();
|
await openAppSettings();
|
||||||
// setState(() {});
|
|
||||||
} else {
|
|
||||||
// if (statuses[Permission.camera]!.isDenied) {
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return statuses;
|
return statuses;
|
||||||
|
|
@ -75,6 +100,7 @@ class PermissionHandlerViewState extends State<PermissionHandlerView> {
|
||||||
try {
|
try {
|
||||||
await permissionServices();
|
await permissionServices();
|
||||||
if (await checkPermissions()) {
|
if (await checkPermissions()) {
|
||||||
|
// ignore: avoid_dynamic_calls
|
||||||
widget.onSuccess();
|
widget.onSuccess();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -573,7 +573,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
if (!showShortReactions) {
|
if (!showShortReactions) {
|
||||||
displayShortReactions();
|
displayShortReactions();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -67,26 +67,24 @@ class _EmojiReactionWidgetState extends State<EmojiReactionWidget> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedSize(
|
return GestureDetector(
|
||||||
key: _targetKey,
|
key: _targetKey,
|
||||||
duration: const Duration(milliseconds: 200),
|
onTap: () async {
|
||||||
curve: Curves.linearToEaseOut,
|
await sendReaction(widget.groupId, widget.messageId, widget.emoji);
|
||||||
child: GestureDetector(
|
widget.emojiKey.currentState?.spawn(
|
||||||
onTap: () async {
|
getGlobalOffset(_targetKey),
|
||||||
await sendReaction(widget.groupId, widget.messageId, widget.emoji);
|
widget.emoji,
|
||||||
widget.emojiKey.currentState?.spawn(
|
);
|
||||||
getGlobalOffset(_targetKey),
|
widget.hide();
|
||||||
widget.emoji,
|
},
|
||||||
);
|
child: SizedBox(
|
||||||
widget.hide();
|
width: 40,
|
||||||
},
|
child: Center(
|
||||||
child: SizedBox(
|
child: widget.show
|
||||||
width: widget.show ? 40 : 10,
|
? EmojiAnimationComp(
|
||||||
child: Center(
|
emoji: widget.emoji,
|
||||||
child: EmojiAnimationComp(
|
)
|
||||||
emoji: widget.emoji,
|
: const SizedBox.shrink(),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class ReactionButtons extends StatefulWidget {
|
||||||
class _ReactionButtonsState extends State<ReactionButtons> {
|
class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
int selectedShortReaction = -1;
|
int selectedShortReaction = -1;
|
||||||
final GlobalKey _keyEmojiPicker = GlobalKey();
|
final GlobalKey _keyEmojiPicker = GlobalKey();
|
||||||
|
bool _renderAnimations = false;
|
||||||
|
|
||||||
List<String> selectedEmojis = EmojiAnimationComp.animatedIcons.keys
|
List<String> selectedEmojis = EmojiAnimationComp.animatedIcons.keys
|
||||||
.toList()
|
.toList()
|
||||||
|
|
@ -47,9 +48,28 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_renderAnimations = widget.show;
|
||||||
initAsync();
|
initAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ReactionButtons oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.show != oldWidget.show) {
|
||||||
|
if (widget.show) {
|
||||||
|
_renderAnimations = true;
|
||||||
|
} else {
|
||||||
|
Future.delayed(const Duration(milliseconds: 150), () {
|
||||||
|
if (mounted && !widget.show) {
|
||||||
|
setState(() {
|
||||||
|
_renderAnimations = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> initAsync() async {
|
Future<void> initAsync() async {
|
||||||
if (userService.currentUser.preSelectedEmojies != null) {
|
if (userService.currentUser.preSelectedEmojies != null) {
|
||||||
selectedEmojis = userService.currentUser.preSelectedEmojies!;
|
selectedEmojis = userService.currentUser.preSelectedEmojies!;
|
||||||
|
|
@ -71,91 +91,92 @@ class _ReactionButtonsState extends State<ReactionButtons> {
|
||||||
? 50
|
? 50
|
||||||
: widget.mediaViewerDistanceFromBottom)
|
: widget.mediaViewerDistanceFromBottom)
|
||||||
: widget.mediaViewerDistanceFromBottom - 20,
|
: widget.mediaViewerDistanceFromBottom - 20,
|
||||||
left: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
left: 0,
|
||||||
right: widget.show ? 0 : MediaQuery.sizeOf(context).width / 2,
|
right: 0,
|
||||||
curve: Curves.linearToEaseOut,
|
curve: Curves.linearToEaseOut,
|
||||||
child: AnimatedOpacity(
|
child: IgnorePointer(
|
||||||
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
ignoring: !widget.show,
|
||||||
duration: const Duration(milliseconds: 150),
|
child: AnimatedOpacity(
|
||||||
child: Container(
|
opacity: widget.show ? 1.0 : 0.0, // Fade in/out
|
||||||
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
duration: const Duration(milliseconds: 150),
|
||||||
padding: widget.show
|
child: Container(
|
||||||
? const EdgeInsets.symmetric(vertical: 32)
|
color: widget.show ? Colors.black.withAlpha(0) : Colors.transparent,
|
||||||
: null,
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (secondRowEmojis.isNotEmpty)
|
if (secondRowEmojis.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: secondRowEmojis
|
||||||
|
.map(
|
||||||
|
(emoji) => EmojiReactionWidget(
|
||||||
|
messageId: widget.messageId,
|
||||||
|
groupId: widget.groupId,
|
||||||
|
hide: widget.hide,
|
||||||
|
show: _renderAnimations,
|
||||||
|
emoji: emoji as String,
|
||||||
|
emojiKey: widget.emojiKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: secondRowEmojis
|
children: [
|
||||||
.map(
|
...firstRowEmojis.map(
|
||||||
(emoji) => EmojiReactionWidget(
|
(emoji) => EmojiReactionWidget(
|
||||||
messageId: widget.messageId,
|
messageId: widget.messageId,
|
||||||
groupId: widget.groupId,
|
groupId: widget.groupId,
|
||||||
hide: widget.hide,
|
hide: widget.hide,
|
||||||
show: widget.show,
|
show: _renderAnimations,
|
||||||
emoji: emoji as String,
|
emoji: emoji,
|
||||||
emojiKey: widget.emojiKey,
|
emojiKey: widget.emojiKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
key: _keyEmojiPicker,
|
||||||
|
onTap: () async {
|
||||||
|
final layer =
|
||||||
|
// ignore: inference_failure_on_function_invocation
|
||||||
|
await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: context.color.surface,
|
||||||
|
builder: (context) {
|
||||||
|
return const EmojiPickerBottom();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
as EmojiLayerData?;
|
||||||
|
if (layer == null) return;
|
||||||
|
await sendReaction(
|
||||||
|
widget.groupId,
|
||||||
|
widget.messageId,
|
||||||
|
layer.text,
|
||||||
|
);
|
||||||
|
widget.emojiKey.currentState?.spawn(
|
||||||
|
getGlobalOffset(_keyEmojiPicker),
|
||||||
|
layer.text,
|
||||||
|
);
|
||||||
|
widget.hide();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.color.surfaceContainer.withAlpha(100),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
)
|
padding: const EdgeInsets.all(8),
|
||||||
.toList(),
|
child: const FaIcon(
|
||||||
|
FontAwesomeIcons.ellipsisVertical,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (secondRowEmojis.isNotEmpty) const SizedBox(height: 15),
|
],
|
||||||
Row(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
...firstRowEmojis.map(
|
|
||||||
(emoji) => EmojiReactionWidget(
|
|
||||||
messageId: widget.messageId,
|
|
||||||
groupId: widget.groupId,
|
|
||||||
hide: widget.hide,
|
|
||||||
show: widget.show,
|
|
||||||
emoji: emoji,
|
|
||||||
emojiKey: widget.emojiKey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
key: _keyEmojiPicker,
|
|
||||||
onTap: () async {
|
|
||||||
final layer =
|
|
||||||
// ignore: inference_failure_on_function_invocation
|
|
||||||
await showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
backgroundColor: context.color.surface,
|
|
||||||
builder: (context) {
|
|
||||||
return const EmojiPickerBottom();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
as EmojiLayerData?;
|
|
||||||
if (layer == null) return;
|
|
||||||
await sendReaction(
|
|
||||||
widget.groupId,
|
|
||||||
widget.messageId,
|
|
||||||
layer.text,
|
|
||||||
);
|
|
||||||
widget.emojiKey.currentState?.spawn(
|
|
||||||
getGlobalOffset(_keyEmojiPicker),
|
|
||||||
layer.text,
|
|
||||||
);
|
|
||||||
widget.hide();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.color.surfaceContainer.withAlpha(100),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: const FaIcon(
|
|
||||||
FontAwesomeIcons.ellipsisVertical,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -55,15 +55,29 @@ impl BackupArchive {
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_db {
|
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.
|
||||||
|
let temp_copy_path = backup_data_dir.join(format!("{}.temp_copy", file_name));
|
||||||
|
std::fs::copy(&file_path, &temp_copy_path)?;
|
||||||
|
|
||||||
let db = Database::new(
|
let db = Database::new(
|
||||||
&file_path.display().to_string(),
|
&temp_copy_path.display().to_string(),
|
||||||
encryption_key.as_deref(),
|
encryption_key.as_deref(),
|
||||||
false,
|
false, // Open the copy in write mode required for encrypted backups
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let backup_database_file = backup_data_dir.join(file_name).display().to_string();
|
let backup_database_file = backup_data_dir.join(file_name).display().to_string();
|
||||||
db.create_backup(backup_database_file.as_str(), encryption_key.as_deref())
|
db.create_backup(backup_database_file.as_str(), encryption_key.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Close database connection to release file lock before removing it
|
||||||
|
drop(db);
|
||||||
|
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?;
|
||||||
|
backup_db.check_integrity().await?;
|
||||||
} else {
|
} else {
|
||||||
let file_backup = backup_data_dir.join(file_name);
|
let file_backup = backup_data_dir.join(file_name);
|
||||||
std::fs::copy(file_path, file_backup)?;
|
std::fs::copy(file_path, file_backup)?;
|
||||||
|
|
|
||||||
|
|
@ -63,14 +63,14 @@ impl Context {
|
||||||
key_manager.store_to_keychain(&secure_storage)?;
|
key_manager.store_to_keychain(&secure_storage)?;
|
||||||
|
|
||||||
let rust_db_path = database_dir.join("rust_db.sqlite");
|
let rust_db_path = database_dir.join("rust_db.sqlite");
|
||||||
let rust_db = Arc::new(
|
let rust_db = Database::new(
|
||||||
Database::new(
|
&rust_db_path.display().to_string(),
|
||||||
&rust_db_path.display().to_string(),
|
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
||||||
Some(&key_manager.main_key.get_database_key(DatabaseKey::RustDb)),
|
false,
|
||||||
false,
|
)
|
||||||
)
|
.await?;
|
||||||
.await?,
|
rust_db.run_migrations().await?;
|
||||||
);
|
let rust_db = Arc::new(rust_db);
|
||||||
|
|
||||||
Ok(Context::from_standalone(TwonlyStandalone {
|
Ok(Context::from_standalone(TwonlyStandalone {
|
||||||
config,
|
config,
|
||||||
|
|
@ -120,14 +120,14 @@ impl Context {
|
||||||
|
|
||||||
let mut rust_db_key = key_manager.main_key.get_database_key(DatabaseKey::RustDb);
|
let mut rust_db_key = key_manager.main_key.get_database_key(DatabaseKey::RustDb);
|
||||||
|
|
||||||
let rust_db = Arc::new(
|
let rust_db = Database::new(
|
||||||
Database::new(
|
&rust_db_path.display().to_string(),
|
||||||
&rust_db_path.display().to_string(),
|
Some(rust_db_key.as_str()),
|
||||||
Some(rust_db_key.as_str()),
|
false,
|
||||||
false,
|
)
|
||||||
)
|
.await?;
|
||||||
.await?,
|
rust_db.run_migrations().await?;
|
||||||
);
|
let rust_db = Arc::new(rust_db);
|
||||||
|
|
||||||
rust_db_key.zeroize();
|
rust_db_key.zeroize();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ impl Database {
|
||||||
let mut connect_options = format!("{db_url}?mode=rwc")
|
let mut connect_options = format!("{db_url}?mode=rwc")
|
||||||
.parse::<SqliteConnectOptions>()?
|
.parse::<SqliteConnectOptions>()?
|
||||||
.log_statements(log_statements_level)
|
.log_statements(log_statements_level)
|
||||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
.journal_mode(sqlx::sqlite::SqliteJournalMode::Delete)
|
||||||
.foreign_keys(true)
|
.foreign_keys(true)
|
||||||
.read_only(read_only)
|
.read_only(read_only)
|
||||||
.busy_timeout(Duration::from_millis(5000))
|
.busy_timeout(Duration::from_millis(5000))
|
||||||
|
.pragma("synchronous", "FULL")
|
||||||
.pragma("recursive_triggers", "ON")
|
.pragma("recursive_triggers", "ON")
|
||||||
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
.log_slow_statements(tracing::log::LevelFilter::Warn, Duration::from_millis(500));
|
||||||
|
|
||||||
|
|
@ -43,15 +44,18 @@ impl Database {
|
||||||
.connect_with(connect_options)
|
.connect_with(connect_options)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run_migrations(&self) -> Result<()> {
|
||||||
sqlx::migrate!("./src/database/migrations")
|
sqlx::migrate!("./src/database/migrations")
|
||||||
.run(&pool)
|
.run(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
tracing::error!("migration error: {:?}", e);
|
tracing::error!("migration error: {:?}", e);
|
||||||
TwonlyError::Generic(format!("Migration error: {}", e))
|
TwonlyError::Generic(format!("Migration error: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
Ok(())
|
||||||
Ok(Self { pool })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn create_backup(
|
pub(crate) async fn create_backup(
|
||||||
|
|
@ -91,6 +95,22 @@ impl Database {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn check_integrity(&self) -> Result<()> {
|
||||||
|
let row: (String,) = sqlx::query_as("PRAGMA integrity_check")
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| TwonlyError::Generic(format!("Integrity check query failed: {}", e)))?;
|
||||||
|
|
||||||
|
if row.0.to_lowercase() == "ok" {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(TwonlyError::Generic(format!(
|
||||||
|
"Database integrity check failed: {}",
|
||||||
|
row.0
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -109,6 +129,7 @@ mod tests {
|
||||||
|
|
||||||
// 1. Create and initialize database with key
|
// 1. Create and initialize database with key
|
||||||
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -137,6 +158,7 @@ mod tests {
|
||||||
let key = "secure_password";
|
let key = "secure_password";
|
||||||
|
|
||||||
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
let db = Database::new(&db_path, Some(key), false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -165,6 +187,7 @@ mod tests {
|
||||||
let backup_path = dir.path().join("backup_plain.sqlite").display().to_string();
|
let backup_path = dir.path().join("backup_plain.sqlite").display().to_string();
|
||||||
|
|
||||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
ReceivedMessage::insert(&db.pool, 1, b"hello world")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ macro_rules! generate_table_tests {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
|
|
||||||
let _id = $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
let _id = $struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
||||||
let all = $struct::$select_all_fn(&db.pool).await.unwrap();
|
let all = $struct::$select_all_fn(&db.pool).await.unwrap();
|
||||||
|
|
@ -92,6 +93,7 @@ macro_rules! generate_test_select {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let db_path = dir.path().join("test.sqlite").display().to_string();
|
let db_path = dir.path().join("test.sqlite").display().to_string();
|
||||||
let db = Database::new(&db_path, None, false).await.unwrap();
|
let db = Database::new(&db_path, None, false).await.unwrap();
|
||||||
|
db.run_migrations().await.unwrap();
|
||||||
|
|
||||||
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
$struct::$insert_fn(&db.pool, $($arg),+).await.unwrap();
|
||||||
let results = $struct::$select_fn(&db.pool, $($sel_arg),+).await.unwrap();
|
let results = $struct::$select_fn(&db.pool, $($sel_arg),+).await.unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue