diff --git a/lib/src/database/daos/mediafiles.dao.dart b/lib/src/database/daos/mediafiles.dao.dart index 78f2d92f..889beb4e 100644 --- a/lib/src/database/daos/mediafiles.dao.dart +++ b/lib/src/database/daos/mediafiles.dao.dart @@ -1,12 +1,13 @@ import 'package:drift/drift.dart'; import 'package:hashlib/random.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; +import 'package:twonly/src/database/tables/messages.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/utils/log.dart'; part 'mediafiles.dao.g.dart'; -@DriftAccessor(tables: [MediaFiles]) +@DriftAccessor(tables: [MediaFiles, Messages]) class MediaFilesDao extends DatabaseAccessor with _$MediaFilesDaoMixin { // this constructor is required so that the main database can create an instance @@ -142,7 +143,9 @@ class MediaFilesDao extends DatabaseAccessor final query = (select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) ..groupBy([ - const CustomExpression('COALESCE(stored_file_hash, media_id)') + const CustomExpression( + 'COALESCE(stored_file_hash, media_id)', + ), ]); return query.map((row) => row.readTable(mediaFiles)).watch(); } @@ -154,6 +157,17 @@ class MediaFilesDao extends DatabaseAccessor .watch(); } + Stream> watchMediaFilesForGroup(String groupId) { + final query = select(mediaFiles).join([ + innerJoin( + db.messages, + db.messages.mediaId.equalsExp(mediaFiles.mediaId), + useColumns: false, + ), + ])..where(db.messages.groupId.equals(groupId)); + return query.map((row) => row.readTable(mediaFiles)).watch(); + } + Future updateAllRetransmissionUploadingState() async { await (update(mediaFiles)..where( (t) => diff --git a/lib/src/database/daos/mediafiles.dao.g.dart b/lib/src/database/daos/mediafiles.dao.g.dart index 3bc0ce86..6d9c5f56 100644 --- a/lib/src/database/daos/mediafiles.dao.g.dart +++ b/lib/src/database/daos/mediafiles.dao.g.dart @@ -5,6 +5,9 @@ part of 'mediafiles.dao.dart'; // ignore_for_file: type=lint mixin _$MediaFilesDaoMixin on DatabaseAccessor { $MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; + $GroupsTable get groups => attachedDatabase.groups; + $ContactsTable get contacts => attachedDatabase.contacts; + $MessagesTable get messages => attachedDatabase.messages; MediaFilesDaoManager get managers => MediaFilesDaoManager(this); } @@ -13,4 +16,10 @@ class MediaFilesDaoManager { MediaFilesDaoManager(this._db); $$MediaFilesTableTableManager get mediaFiles => $$MediaFilesTableTableManager(_db.attachedDatabase, _db.mediaFiles); + $$GroupsTableTableManager get groups => + $$GroupsTableTableManager(_db.attachedDatabase, _db.groups); + $$ContactsTableTableManager get contacts => + $$ContactsTableTableManager(_db.attachedDatabase, _db.contacts); + $$MessagesTableTableManager get messages => + $$MessagesTableTableManager(_db.attachedDatabase, _db.messages); } diff --git a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart index c9d85db6..8c773cb0 100644 --- a/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/group_list_item.comp.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:mutex/mutex.dart'; + import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; @@ -70,36 +70,38 @@ class _UserListItem extends State { ); if (!mounted) return; _lastMessageStream = lastMsgStream.listen((update) { - protectUpdateState.protect(() async { - await updateState(update, _messagesNotOpened); - }); + _updateState(update, _messagesNotOpened); }); - _lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) { - if (!mounted) return; - setState(() { - _lastReaction = update; - }); - }); + _lastReactionStream = twonlyDB.reactionsDao + .watchLastReactions(widget.group.groupId) + .listen((update) { + if (!mounted) return; + setState(() { + _lastReaction = update; + }); + }); - _messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) { - protectUpdateState.protect(() async { - await updateState(_lastMessage, update); - }); - }); + _messagesNotOpenedStream = twonlyDB.messagesDao + .watchMessageNotOpened(widget.group.groupId) + .listen((update) { + _updateState(_lastMessage, update); + }); - _lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { - if (!mounted) return; - for (final mediaFile in mediaFiles) { - final index = _previewMediaFiles.indexWhere( - (t) => t.mediaId == mediaFile.mediaId, - ); - if (index >= 0) { - _previewMediaFiles[index] = mediaFile; - } - } - setState(() {}); - }); + _lastMediaFilesStream = twonlyDB.mediaFilesDao + .watchMediaFilesForGroup(widget.group.groupId) + .listen((mediaFiles) { + if (!mounted) return; + for (final mediaFile in mediaFiles) { + final index = _previewMediaFiles.indexWhere( + (t) => t.mediaId == mediaFile.mediaId, + ); + if (index >= 0) { + _previewMediaFiles[index] = mediaFile; + } + } + setState(() {}); + }); final groupContacts = await twonlyDB.groupsDao.getGroupContact( widget.group.groupId, @@ -110,19 +112,21 @@ class _UserListItem extends State { } } - Mutex protectUpdateState = Mutex(); - - Future updateState( + void _updateState( Message? newLastMessage, List newMessagesNotOpened, - ) async { + ) { + if (!mounted) return; if (newLastMessage == null) { // there are no messages at all _currentMessage = null; _previewMessages = []; } else if (newMessagesNotOpened.isNotEmpty) { - // Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. - final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList(); + // Filter for the preview non opened messages. First messages which where + // send but not yet opened by the other side. + final receivedMessages = newMessagesNotOpened + .where((x) => x.senderId != null) + .toList(); if (receivedMessages.isNotEmpty) { _previewMessages = receivedMessages; @@ -133,8 +137,9 @@ class _UserListItem extends State { } } else { // there are no not opened messages show just the last message in the table - // only shows the last message in case there was no newer messages which already got deleted - // This prevents, that it will show that a images got stored 10 days ago... + // only shows the last message in case there was no newer messages which + // already got deleted. This prevents showing that an image got stored 10 + // days ago... if (newLastMessage.createdAt.isAfter( widget.group.lastMessageExchange.subtract(const Duration(days: 2)), )) { @@ -146,7 +151,9 @@ class _UserListItem extends State { } } - final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList(); + final msgs = _previewMessages + .where((x) => x.type == MessageType.media.name) + .toList(); if (msgs.isNotEmpty && msgs.first.type == MessageType.media.name && !msgs.first.isDeletedFromSender && @@ -157,20 +164,30 @@ class _UserListItem extends State { _hasNonOpenedMediaFile = false; } + _lastMessage = newLastMessage; + _messagesNotOpened = newMessagesNotOpened; + setState(() {}); + + // Only fetch on first load when a mediaId is not yet cached. + _fetchMissingMediaFiles(); + } + + /// Fetches any media files referenced by preview messages but not yet in the + /// local cache. Fire-and-forget; updates state when results arrive. + Future _fetchMissingMediaFiles() async { for (final message in _previewMessages) { - if (message.mediaId != null && !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { + if (message.mediaId != null && + !_previewMediaFiles.any((t) => t.mediaId == message.mediaId)) { final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( message.mediaId!, ); - if (mediaFile != null) { - _previewMediaFiles.add(mediaFile); + if (mediaFile != null && mounted) { + setState(() { + _previewMediaFiles.add(mediaFile); + }); } } } - - _lastMessage = newLastMessage; - _messagesNotOpened = newMessagesNotOpened; - if (mounted) setState(() {}); } Future onTap() async { @@ -183,7 +200,9 @@ class _UserListItem extends State { } if (_hasNonOpenedMediaFile) { - final msgs = _previewMessages.where((x) => x.type == MessageType.media.name).toList(); + final msgs = _previewMessages + .where((x) => x.type == MessageType.media.name) + .toList(); final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById( msgs.first.mediaId!, ); @@ -223,7 +242,10 @@ class _UserListItem extends State { const SizedBox(width: 3), VerificationBadgeComp( group: widget.group, - showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList, + showOnlyIfVerified: userService + .currentUser + .securityProfile + .showOnlyVerifiedInChatViewList, clickable: false, size: 12, ), @@ -258,7 +280,8 @@ class _UserListItem extends State { ), const Text('•'), const SizedBox(width: 5), - if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage), + if (_currentMessage != null) + LastMessageTimeComp(message: _currentMessage), FlameCounterWidget( groupId: widget.group.groupId, prefix: true, @@ -272,7 +295,9 @@ class _UserListItem extends State { widget.group.groupId, ); if (!context.mounted) return; - await context.push(Routes.profileContact(contacts.first.userId)); + await context.push( + Routes.profileContact(contacts.first.userId), + ); return; } else { await context.push(Routes.profileGroup(widget.group.groupId)); @@ -285,7 +310,9 @@ class _UserListItem extends State { : IconButton( onPressed: () { if (_hasNonOpenedMediaFile) { - context.push(Routes.chatsMessages(widget.group.groupId)); + context.push( + Routes.chatsMessages(widget.group.groupId), + ); } else { context.push( Routes.chatsCameraSendTo, @@ -294,7 +321,9 @@ class _UserListItem extends State { } }, icon: FaIcon( - _hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera, + _hasNonOpenedMediaFile + ? FontAwesomeIcons.solidComments + : FontAwesomeIcons.camera, color: context.color.outline.withAlpha(150), ), ),