fix: messages where opene even if the app is in the background

This commit is contained in:
otsmr 2026-04-27 19:50:28 +02:00
parent 5b5140ec7c
commit a93187c86d
3 changed files with 60 additions and 29 deletions

View file

@ -161,18 +161,6 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
} }
} }
Future<void> openedAllTextMessages(String groupId) {
final updates = MessagesCompanion(openedAt: Value(clock.now()));
return (update(messages)..where(
(t) =>
t.groupId.equals(groupId) &
t.senderId.isNotNull() &
t.openedAt.isNull() &
t.type.equals(MessageType.text.name),
))
.write(updates);
}
Future<void> handleMessageDeletion( Future<void> handleMessageDeletion(
int? contactId, int? contactId,
String messageId, String messageId,
@ -184,13 +172,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDB> with _$MessagesDaoMixin {
return; return;
} }
if (msg.mediaId != null && contactId != null) { if (msg.mediaId != null && contactId != null) {
final otherMessagesWithSameMedia = await (select(messages) final otherMessagesWithSameMedia =
..where( await (select(messages)..where(
(t) => (t) =>
t.mediaId.equals(msg.mediaId!) & t.mediaId.equals(msg.mediaId!) &
t.messageId.equals(messageId).not(), t.messageId.equals(messageId).not(),
)) ))
.get(); .get();
if (otherMessagesWithSameMedia.isEmpty) { if (otherMessagesWithSameMedia.isEmpty) {
await (delete( await (delete(

View file

@ -6,6 +6,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -36,7 +37,7 @@ class ChatMessagesView extends StatefulWidget {
State<ChatMessagesView> createState() => _ChatMessagesViewState(); State<ChatMessagesView> createState() => _ChatMessagesViewState();
} }
class _ChatMessagesViewState extends State<ChatMessagesView> { class _ChatMessagesViewState extends State<ChatMessagesView> with WidgetsBindingObserver {
HashSet<int> alreadyReportedOpened = HashSet<int>(); HashSet<int> alreadyReportedOpened = HashSet<int>();
late StreamSubscription<Group?> userSub; late StreamSubscription<Group?> userSub;
late StreamSubscription<List<Message>> messageSub; late StreamSubscription<List<Message>> messageSub;
@ -64,6 +65,7 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
void initState() { void initState() {
super.initState(); super.initState();
textFieldFocus = FocusNode(); textFieldFocus = FocusNode();
WidgetsBinding.instance.addObserver(this);
initStreams(); initStreams();
} }
@ -74,11 +76,26 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
contactSub?.cancel(); contactSub?.cancel();
groupActionsSub?.cancel(); groupActionsSub?.cancel();
_nextTypingIndicator?.cancel(); _nextTypingIndicator?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
Mutex protectMessageUpdating = Mutex(); Mutex protectMessageUpdating = Mutex();
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
protectMessageUpdating.protect(() async {
await setMessages(allMessages, groupActions);
});
}
}
bool _isViewActive() {
return !AppState.isAppInBackground &&
(ModalRoute.of(context)?.isCurrent ?? false);
}
Future<void> initStreams() async { Future<void> initStreams() async {
final groupStream = twonlyDB.groupsDao.watchGroup(widget.groupId); final groupStream = twonlyDB.groupsDao.watchGroup(widget.groupId);
userSub = groupStream.listen((newGroup) { userSub = groupStream.listen((newGroup) {
@ -137,7 +154,9 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
List<Message> newMessages, List<Message> newMessages,
List<GroupHistory> groupActions, List<GroupHistory> groupActions,
) async { ) async {
await flutterLocalNotificationsPlugin.cancelAll(); if (_isViewActive()) {
await flutterLocalNotificationsPlugin.cancelAll();
}
final chatItems = <ChatItem>[]; final chatItems = <ChatItem>[];
final storedMediaFiles = <Message>[]; final storedMediaFiles = <Message>[];
@ -189,11 +208,13 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
} }
} }
for (final contactId in openedMessages.keys) { if (_isViewActive()) {
await notifyContactAboutOpeningMessage( for (final contactId in openedMessages.keys) {
contactId, await notifyContactAboutOpeningMessage(
openedMessages[contactId]!, contactId,
); openedMessages[contactId]!,
);
}
} }
if (!mounted) return; if (!mounted) return;

View file

@ -10,6 +10,7 @@ import 'package:lottie/lottie.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:no_screenshot/no_screenshot.dart'; import 'package:no_screenshot/no_screenshot.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart';
@ -46,7 +47,8 @@ class MediaViewerView extends StatefulWidget {
State<MediaViewerView> createState() => _MediaViewerViewState(); State<MediaViewerView> createState() => _MediaViewerViewState();
} }
class _MediaViewerViewState extends State<MediaViewerView> { class _MediaViewerViewState extends State<MediaViewerView>
with WidgetsBindingObserver {
Timer? nextMediaTimer; Timer? nextMediaTimer;
Timer? progressTimer; Timer? progressTimer;
@ -87,6 +89,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
if (widget.initialMessage != null) { if (widget.initialMessage != null) {
allMediaFiles = [widget.initialMessage!]; allMediaFiles = [widget.initialMessage!];
} }
WidgetsBinding.instance.addObserver(this);
asyncLoadNextMedia(true); asyncLoadNextMedia(true);
} }
@ -101,11 +104,28 @@ class _MediaViewerViewState extends State<MediaViewerView> {
final tmp = videoController; final tmp = videoController;
videoController = null; videoController = null;
tmp?.dispose(); tmp?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
final Mutex _messageUpdateLock = Mutex(); final Mutex _messageUpdateLock = Mutex();
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_messageUpdateLock.protect(() async {
if (currentMedia == null && allMediaFiles.isNotEmpty) {
await loadCurrentMediaFile();
}
});
}
}
bool _isViewActive() {
return !AppState.isAppInBackground &&
(ModalRoute.of(context)?.isCurrent ?? false);
}
Future<void> asyncLoadNextMedia(bool firstRun) async { Future<void> asyncLoadNextMedia(bool firstRun) async {
_subscription = twonlyDB.messagesDao _subscription = twonlyDB.messagesDao
.watchMediaNotOpened(widget.group.groupId) .watchMediaNotOpened(widget.group.groupId)
@ -195,7 +215,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
showSendTextMessageInput = false; showSendTextMessageInput = false;
}); });
unawaited(flutterLocalNotificationsPlugin.cancelAll()); if (_isViewActive()) {
unawaited(flutterLocalNotificationsPlugin.cancelAll());
}
final stream = twonlyDB.mediaFilesDao.watchMedia( final stream = twonlyDB.mediaFilesDao.watchMedia(
allMediaFiles.first.mediaId!, allMediaFiles.first.mediaId!,