fix performance issue

This commit is contained in:
otsmr 2026-07-07 21:26:42 +02:00
parent 9faa4fc49a
commit 7a8901cd2d
3 changed files with 103 additions and 51 deletions

View file

@ -1,12 +1,13 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hashlib/random.dart'; import 'package:hashlib/random.dart';
import 'package:twonly/src/database/tables/mediafiles.table.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/database/twonly.db.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
part 'mediafiles.dao.g.dart'; part 'mediafiles.dao.g.dart';
@DriftAccessor(tables: [MediaFiles]) @DriftAccessor(tables: [MediaFiles, Messages])
class MediaFilesDao extends DatabaseAccessor<TwonlyDB> class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
with _$MediaFilesDaoMixin { with _$MediaFilesDaoMixin {
// this constructor is required so that the main database can create an instance // this constructor is required so that the main database can create an instance
@ -142,7 +143,9 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
final query = final query =
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) (select(mediaFiles)..where((t) => t.stored.equals(true))).join([])
..groupBy([ ..groupBy([
const CustomExpression<Object>('COALESCE(stored_file_hash, media_id)') const CustomExpression<Object>(
'COALESCE(stored_file_hash, media_id)',
),
]); ]);
return query.map((row) => row.readTable(mediaFiles)).watch(); return query.map((row) => row.readTable(mediaFiles)).watch();
} }
@ -154,6 +157,17 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
.watch(); .watch();
} }
Stream<List<MediaFile>> 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<void> updateAllRetransmissionUploadingState() async { Future<void> updateAllRetransmissionUploadingState() async {
await (update(mediaFiles)..where( await (update(mediaFiles)..where(
(t) => (t) =>

View file

@ -5,6 +5,9 @@ part of 'mediafiles.dao.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> { mixin _$MediaFilesDaoMixin on DatabaseAccessor<TwonlyDB> {
$MediaFilesTable get mediaFiles => attachedDatabase.mediaFiles; $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); MediaFilesDaoManager get managers => MediaFilesDaoManager(this);
} }
@ -13,4 +16,10 @@ class MediaFilesDaoManager {
MediaFilesDaoManager(this._db); MediaFilesDaoManager(this._db);
$$MediaFilesTableTableManager get mediaFiles => $$MediaFilesTableTableManager get mediaFiles =>
$$MediaFilesTableTableManager(_db.attachedDatabase, _db.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);
} }

View file

@ -3,7 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; 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: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';
@ -70,36 +70,38 @@ class _UserListItem extends State<GroupListItemComp> {
); );
if (!mounted) return; if (!mounted) return;
_lastMessageStream = lastMsgStream.listen((update) { _lastMessageStream = lastMsgStream.listen((update) {
protectUpdateState.protect(() async { _updateState(update, _messagesNotOpened);
await updateState(update, _messagesNotOpened);
});
}); });
_lastReactionStream = twonlyDB.reactionsDao.watchLastReactions(widget.group.groupId).listen((update) { _lastReactionStream = twonlyDB.reactionsDao
if (!mounted) return; .watchLastReactions(widget.group.groupId)
setState(() { .listen((update) {
_lastReaction = update; if (!mounted) return;
}); setState(() {
}); _lastReaction = update;
});
});
_messagesNotOpenedStream = twonlyDB.messagesDao.watchMessageNotOpened(widget.group.groupId).listen((update) { _messagesNotOpenedStream = twonlyDB.messagesDao
protectUpdateState.protect(() async { .watchMessageNotOpened(widget.group.groupId)
await updateState(_lastMessage, update); .listen((update) {
}); _updateState(_lastMessage, update);
}); });
_lastMediaFilesStream = twonlyDB.mediaFilesDao.watchNewestMediaFiles().listen((mediaFiles) { _lastMediaFilesStream = twonlyDB.mediaFilesDao
if (!mounted) return; .watchMediaFilesForGroup(widget.group.groupId)
for (final mediaFile in mediaFiles) { .listen((mediaFiles) {
final index = _previewMediaFiles.indexWhere( if (!mounted) return;
(t) => t.mediaId == mediaFile.mediaId, for (final mediaFile in mediaFiles) {
); final index = _previewMediaFiles.indexWhere(
if (index >= 0) { (t) => t.mediaId == mediaFile.mediaId,
_previewMediaFiles[index] = mediaFile; );
} if (index >= 0) {
} _previewMediaFiles[index] = mediaFile;
setState(() {}); }
}); }
setState(() {});
});
final groupContacts = await twonlyDB.groupsDao.getGroupContact( final groupContacts = await twonlyDB.groupsDao.getGroupContact(
widget.group.groupId, widget.group.groupId,
@ -110,19 +112,21 @@ class _UserListItem extends State<GroupListItemComp> {
} }
} }
Mutex protectUpdateState = Mutex(); void _updateState(
Future<void> updateState(
Message? newLastMessage, Message? newLastMessage,
List<Message> newMessagesNotOpened, List<Message> newMessagesNotOpened,
) async { ) {
if (!mounted) return;
if (newLastMessage == null) { if (newLastMessage == null) {
// there are no messages at all // there are no messages at all
_currentMessage = null; _currentMessage = null;
_previewMessages = []; _previewMessages = [];
} else if (newMessagesNotOpened.isNotEmpty) { } else if (newMessagesNotOpened.isNotEmpty) {
// Filter for the preview non opened messages. First messages which where send but not yet opened by the other side. // Filter for the preview non opened messages. First messages which where
final receivedMessages = newMessagesNotOpened.where((x) => x.senderId != null).toList(); // send but not yet opened by the other side.
final receivedMessages = newMessagesNotOpened
.where((x) => x.senderId != null)
.toList();
if (receivedMessages.isNotEmpty) { if (receivedMessages.isNotEmpty) {
_previewMessages = receivedMessages; _previewMessages = receivedMessages;
@ -133,8 +137,9 @@ class _UserListItem extends State<GroupListItemComp> {
} }
} else { } else {
// there are no not opened messages show just the last message in the table // 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 // only shows the last message in case there was no newer messages which
// This prevents, that it will show that a images got stored 10 days ago... // already got deleted. This prevents showing that an image got stored 10
// days ago...
if (newLastMessage.createdAt.isAfter( if (newLastMessage.createdAt.isAfter(
widget.group.lastMessageExchange.subtract(const Duration(days: 2)), widget.group.lastMessageExchange.subtract(const Duration(days: 2)),
)) { )) {
@ -146,7 +151,9 @@ class _UserListItem extends State<GroupListItemComp> {
} }
} }
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 && if (msgs.isNotEmpty &&
msgs.first.type == MessageType.media.name && msgs.first.type == MessageType.media.name &&
!msgs.first.isDeletedFromSender && !msgs.first.isDeletedFromSender &&
@ -157,20 +164,30 @@ class _UserListItem extends State<GroupListItemComp> {
_hasNonOpenedMediaFile = false; _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<void> _fetchMissingMediaFiles() async {
for (final message in _previewMessages) { 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( final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
message.mediaId!, message.mediaId!,
); );
if (mediaFile != null) { if (mediaFile != null && mounted) {
_previewMediaFiles.add(mediaFile); setState(() {
_previewMediaFiles.add(mediaFile);
});
} }
} }
} }
_lastMessage = newLastMessage;
_messagesNotOpened = newMessagesNotOpened;
if (mounted) setState(() {});
} }
Future<void> onTap() async { Future<void> onTap() async {
@ -183,7 +200,9 @@ class _UserListItem extends State<GroupListItemComp> {
} }
if (_hasNonOpenedMediaFile) { 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( final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
msgs.first.mediaId!, msgs.first.mediaId!,
); );
@ -223,7 +242,10 @@ class _UserListItem extends State<GroupListItemComp> {
const SizedBox(width: 3), const SizedBox(width: 3),
VerificationBadgeComp( VerificationBadgeComp(
group: widget.group, group: widget.group,
showOnlyIfVerified: userService.currentUser.securityProfile.showOnlyVerifiedInChatViewList, showOnlyIfVerified: userService
.currentUser
.securityProfile
.showOnlyVerifiedInChatViewList,
clickable: false, clickable: false,
size: 12, size: 12,
), ),
@ -258,7 +280,8 @@ class _UserListItem extends State<GroupListItemComp> {
), ),
const Text(''), const Text(''),
const SizedBox(width: 5), const SizedBox(width: 5),
if (_currentMessage != null) LastMessageTimeComp(message: _currentMessage), if (_currentMessage != null)
LastMessageTimeComp(message: _currentMessage),
FlameCounterWidget( FlameCounterWidget(
groupId: widget.group.groupId, groupId: widget.group.groupId,
prefix: true, prefix: true,
@ -272,7 +295,9 @@ class _UserListItem extends State<GroupListItemComp> {
widget.group.groupId, widget.group.groupId,
); );
if (!context.mounted) return; if (!context.mounted) return;
await context.push(Routes.profileContact(contacts.first.userId)); await context.push(
Routes.profileContact(contacts.first.userId),
);
return; return;
} else { } else {
await context.push(Routes.profileGroup(widget.group.groupId)); await context.push(Routes.profileGroup(widget.group.groupId));
@ -285,7 +310,9 @@ class _UserListItem extends State<GroupListItemComp> {
: IconButton( : IconButton(
onPressed: () { onPressed: () {
if (_hasNonOpenedMediaFile) { if (_hasNonOpenedMediaFile) {
context.push(Routes.chatsMessages(widget.group.groupId)); context.push(
Routes.chatsMessages(widget.group.groupId),
);
} else { } else {
context.push( context.push(
Routes.chatsCameraSendTo, Routes.chatsCameraSendTo,
@ -294,7 +321,9 @@ class _UserListItem extends State<GroupListItemComp> {
} }
}, },
icon: FaIcon( icon: FaIcon(
_hasNonOpenedMediaFile ? FontAwesomeIcons.solidComments : FontAwesomeIcons.camera, _hasNonOpenedMediaFile
? FontAwesomeIcons.solidComments
: FontAwesomeIcons.camera,
color: context.color.outline.withAlpha(150), color: context.color.outline.withAlpha(150),
), ),
), ),