From 18d16fdd57bbdf6548892e5f541df5e63820ed3a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 20 Nov 2025 00:01:36 +0800 Subject: [PATCH] :bug: Fix bugs in message db --- lib/database/drift_db.dart | 72 +++- lib/database/message.dart | 23 +- lib/pods/chat/messages_notifier.dart | 40 +- lib/screens/chat/room.dart | 584 ++++++++------------------- lib/widgets/app_scaffold.dart | 14 +- 5 files changed, 283 insertions(+), 450 deletions(-) diff --git a/lib/database/drift_db.dart b/lib/database/drift_db.dart index 3f3d4cad..60885842 100644 --- a/lib/database/drift_db.dart +++ b/lib/database/drift_db.dart @@ -160,6 +160,7 @@ class AppDatabase extends _$AppDatabase { String roomId, String query, { bool? withAttachments, + Future Function(String accountId)? fetchAccount, }) async { var selectStatement = select(chatMessages) ..where((m) => m.roomId.equals(roomId)); @@ -186,7 +187,9 @@ class AppDatabase extends _$AppDatabase { ..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) .get(); final messageFutures = - messages.map((msg) => companionToMessage(msg)).toList(); + messages + .map((msg) => companionToMessage(msg, fetchAccount: fetchAccount)) + .toList(); return await Future.wait(messageFutures); } @@ -215,18 +218,19 @@ class AppDatabase extends _$AppDatabase { ); } - Future companionToMessage(ChatMessage dbMessage) async { + Future companionToMessage( + ChatMessage dbMessage, { + Future Function(String accountId)? fetchAccount, + }) async { final data = jsonDecode(dbMessage.data); SnChatMember? sender; try { final senderRow = await (select(chatMembers) ..where((m) => m.id.equals(dbMessage.senderId))).getSingle(); - final senderAccount = SnAccount.fromJson(senderRow.account); - SnAccountStatus? senderStatus; - if (senderRow.status != null) { - senderStatus = SnAccountStatus.fromJson(jsonDecode(senderRow.status!)); - } + SnAccount senderAccount; + senderAccount = SnAccount.fromJson(senderRow.account); + sender = SnChatMember( id: senderRow.id, chatRoomId: senderRow.chatRoomId, @@ -239,15 +243,57 @@ class AppDatabase extends _$AppDatabase { breakUntil: senderRow.breakUntil, timeoutUntil: senderRow.timeoutUntil, isBot: senderRow.isBot, - status: senderStatus, + status: null, lastTyped: senderRow.lastTyped, createdAt: senderRow.createdAt, updatedAt: senderRow.updatedAt, deletedAt: senderRow.deletedAt, chatRoom: null, ); - } catch (_) { - sender = null; + } catch (err) { + // Fallback to dummy sender with senderId as display name + sender = SnChatMember( + id: 'unknown', + chatRoomId: dbMessage.roomId, + accountId: dbMessage.senderId, + account: SnAccount( + id: 'unknown', + name: 'unknown', + nick: dbMessage.senderId, // Show the ID instead of Unknown + profile: SnAccountProfile( + picture: null, + id: 'unknown', + experience: 0, + level: 1, + levelingProgress: 0.0, + background: null, + verification: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + deletedAt: null, + ), + language: '', + isSuperuser: false, + automatedId: null, + perkSubscription: null, + deletedAt: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + nick: dbMessage.senderId, // Show the senderId as fallback + role: 0, + notify: 0, + joinedAt: null, + breakUntil: null, + timeoutUntil: null, + isBot: false, + status: null, + lastTyped: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + deletedAt: null, + chatRoom: null, + ); } return LocalChatMessage( id: dbMessage.id, @@ -377,4 +423,10 @@ class AppDatabase extends _$AppDatabase { return await (select(postDrafts) ..where((tbl) => tbl.id.equals(id))).getSingleOrNull(); } + + Future saveMember(SnChatMember member) async { + await into( + chatMembers, + ).insert(companionFromMember(member), mode: InsertMode.insertOrReplace); + } } diff --git a/lib/database/message.dart b/lib/database/message.dart index 7bb99ea9..f3e8ec89 100644 --- a/lib/database/message.dart +++ b/lib/database/message.dart @@ -158,10 +158,11 @@ class LocalChatMessage { }); SnChatMessage toRemoteMessage() { - final msgData = Map.from(data); - if (sender != null) { - msgData['sender'] = sender!.toJson(); + if (sender == null) { + throw Exception('Cannot create remote message without sender'); } + final msgData = Map.from(data); + msgData['sender'] = sender!.toJson(); return SnChatMessage.fromJson(msgData); } @@ -170,8 +171,20 @@ class LocalChatMessage { MessageStatus status, { String? nonce, }) { - final msgData = Map.from(message.toJson()) - ..remove('sender'); + final jsonData = message.toJson(); + jsonData.remove('sender'); + // Ensure proper defaults for collections to prevent type cast errors + if (jsonData['meta'] == null) jsonData['meta'] = {}; + if (jsonData['members_mentioned'] == null) { + jsonData['members_mentioned'] = []; + } + if (jsonData['attachments'] == null) { + jsonData['attachments'] = >[]; + } + if (jsonData['reactions'] == null) { + jsonData['reactions'] = >[]; + } + final msgData = Map.from(jsonData); return LocalChatMessage( id: message.id, roomId: message.chatRoomId, diff --git a/lib/pods/chat/messages_notifier.dart b/lib/pods/chat/messages_notifier.dart index 2cfc7e1c..16e30ed7 100644 --- a/lib/pods/chat/messages_notifier.dart +++ b/lib/pods/chat/messages_notifier.dart @@ -6,6 +6,7 @@ import "package:flutter/material.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:island/database/drift_db.dart"; import "package:island/database/message.dart"; +import "package:island/models/account.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; import "package:island/models/poll.dart"; @@ -20,6 +21,7 @@ import "package:riverpod_annotation/riverpod_annotation.dart"; import "package:uuid/uuid.dart"; import "package:island/screens/chat/chat.dart"; import "package:island/pods/chat/chat_rooms.dart"; +import "package:island/screens/account/profile.dart"; part 'messages_notifier.g.dart'; @@ -45,6 +47,8 @@ class MessagesNotifier extends _$MessagesNotifier { bool _isUpdatingState = false; DateTime? _lastPauseTime; + late final Future Function(String) _fetchAccount; + @override FutureOr> build(String roomId) async { _roomId = roomId; @@ -53,6 +57,15 @@ class MessagesNotifier extends _$MessagesNotifier { final room = await ref.watch(chatroomProvider(roomId).future); final identity = await ref.watch(chatroomIdentityProvider(roomId).future); + // Initialize fetch account method for corrupted data recovery + _fetchAccount = (String accountId) async { + try { + return await ref.watch(accountProvider(accountId).future); + } catch (_) { + return null; + } + }; + if (room == null) { throw Exception('Room not found'); } @@ -133,6 +146,7 @@ class MessagesNotifier extends _$MessagesNotifier { _roomId, searchQuery, withAttachments: withAttachments, + fetchAccount: _fetchAccount, ); } else { final chatMessagesFromDb = await _database.getMessagesForRoom( @@ -142,7 +156,12 @@ class MessagesNotifier extends _$MessagesNotifier { ); dbMessages = await Future.wait( chatMessagesFromDb - .map((msg) => _database.companionToMessage(msg)) + .map( + (msg) => _database.companionToMessage( + msg, + fetchAccount: _fetchAccount, + ), + ) .toList(), ); } @@ -207,7 +226,10 @@ class MessagesNotifier extends _$MessagesNotifier { ); final dbMessages = await Future.wait( chatMessagesFromDb - .map((msg) => _database.companionToMessage(msg)) + .map( + (msg) => + _database.companionToMessage(msg, fetchAccount: _fetchAccount), + ) .toList(), ); @@ -278,6 +300,9 @@ class MessagesNotifier extends _$MessagesNotifier { for (final message in messages) { await _database.saveMessage(_database.messageToCompanion(message)); + if (message.sender != null) { + await _database.saveMember(message.sender!); // Save/update member data + } if (message.nonce != null) { _pendingMessages.removeWhere( (_, pendingMsg) => pendingMsg.nonce == message.nonce, @@ -306,7 +331,10 @@ class MessagesNotifier extends _$MessagesNotifier { final lastMessage = dbMessages.isEmpty ? null - : await _database.companionToMessage(dbMessages.first); + : await _database.companionToMessage( + dbMessages.first, + fetchAccount: _fetchAccount, + ); if (lastMessage == null) { talker.log('No local messages, fetching from network'); @@ -474,6 +502,7 @@ class MessagesNotifier extends _$MessagesNotifier { _pendingMessages[localMessage.id] = localMessage; _fileUploadProgress[localMessage.id] = {}; await _database.saveMessage(_database.messageToCompanion(localMessage)); + await _database.saveMember(mockMessage.sender); final currentMessages = state.value ?? []; state = AsyncValue.data([localMessage, ...currentMessages]); @@ -894,7 +923,10 @@ class MessagesNotifier extends _$MessagesNotifier { await (_database.select(_database.chatMessages) ..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull(); if (localMessage != null) { - return _database.companionToMessage(localMessage); + return _database.companionToMessage( + localMessage, + fetchAccount: _fetchAccount, + ); } final response = await _apiClient.get( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 8bdcebd5..250d8a90 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -148,9 +148,6 @@ class ChatRoomScreen extends HookConsumerWidget { final inputKey = useMemoized(() => GlobalKey()); final inputHeight = useState(80.0); - // Track previous height for smooth animations - final previousInputHeight = usePrevious(inputHeight.value); - // Periodic height measurement for dynamic sizing useEffect(() { final timer = Timer.periodic(const Duration(milliseconds: 50), (_) { @@ -624,428 +621,179 @@ class ChatRoomScreen extends HookConsumerWidget { } } - Widget chatMessageListWidget(List messageList) => - previousInputHeight != null && previousInputHeight != inputHeight.value - ? TweenAnimationBuilder( - tween: Tween( - begin: previousInputHeight, - end: inputHeight.value, - ), - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - builder: - (context, height, child) => SuperListView.builder( - listController: listController, - padding: EdgeInsets.only( - top: 16, - bottom: - MediaQuery.of(context).padding.bottom + 8 + height, - ), - controller: scrollController, - reverse: true, // Show newest messages at the bottom - itemCount: messageList.length, - findChildIndexCallback: (key) { - if (key is! ValueKey) return null; - final messageId = key.value.substring( - messageKeyPrefix.length, - ); - final index = messageList.indexWhere( - (m) => (m.nonce ?? m.id) == messageId, - ); - // Return null for invalid indices to let SuperListView handle it properly - return index >= 0 ? index : null; - }, - extentEstimation: (_, _) => 40, - itemBuilder: (context, index) { - final message = messageList[index]; - final nextMessage = - index < messageList.length - 1 - ? messageList[index + 1] - : null; - final isLastInGroup = - nextMessage == null || - nextMessage.senderId != message.senderId || - nextMessage.createdAt - .difference(message.createdAt) - .inMinutes - .abs() > - 3; + Widget chatMessageListWidget( + List messageList, + ) => AnimatedPadding( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + padding: EdgeInsets.only( + top: 16, + bottom: MediaQuery.of(context).padding.bottom + 8 + inputHeight.value, + ), + child: SuperListView.builder( + listController: listController, + controller: scrollController, + reverse: true, // Show newest messages at the bottom + itemCount: messageList.length, + findChildIndexCallback: (key) { + if (key is! ValueKey) return null; + final messageId = key.value.substring(messageKeyPrefix.length); + final index = messageList.indexWhere( + (m) => (m.nonce ?? m.id) == messageId, + ); + return index >= 0 ? index : null; + }, + extentEstimation: (_, _) => 40, + itemBuilder: (context, index) { + final message = messageList[index]; + final nextMessage = + index < messageList.length - 1 ? messageList[index + 1] : null; + final isLastInGroup = + nextMessage == null || + nextMessage.senderId != message.senderId || + nextMessage.createdAt + .difference(message.createdAt) + .inMinutes + .abs() > + 3; - // Use a stable animation key that doesn't change during message lifecycle - final key = Key( - '$messageKeyPrefix${message.nonce ?? message.id}', - ); + final key = Key('$messageKeyPrefix${message.nonce ?? message.id}'); - final messageWidget = chatIdentity.when( - skipError: true, - data: - (identity) => GestureDetector( - onLongPress: () { - if (!isSelectionMode.value) { - toggleSelectionMode(); - toggleMessageSelection(message.id); - } - }, - onTap: () { - if (isSelectionMode.value) { - toggleMessageSelection(message.id); - } - }, - child: Container( - color: - selectedMessages.value.contains(message.id) - ? Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.3) - : null, - child: Stack( - children: [ - MessageItem( - key: - settings.disableAnimation - ? key - : null, - message: message, - isCurrentUser: - identity?.id == message.senderId, - onAction: - isSelectionMode.value - ? null - : (action) { - switch (action) { - case MessageItemAction.delete: - messagesNotifier - .deleteMessage( - message.id, - ); - case MessageItemAction.edit: - messageEditingTo.value = - message - .toRemoteMessage(); - messageController.text = - messageEditingTo - .value - ?.content ?? - ''; - attachments.value = - messageEditingTo - .value! - .attachments - .map( - (e) => - UniversalFile.fromAttachment( - e, - ), - ) - .toList(); - case MessageItemAction - .forward: - messageForwardingTo.value = - message - .toRemoteMessage(); - case MessageItemAction.reply: - messageReplyingTo.value = - message - .toRemoteMessage(); - case MessageItemAction.resend: - messagesNotifier - .retryMessage( - message.id, - ); - } - }, - onJump: (messageId) { - scrollToMessage( - messageId: messageId, - messageList: messageList, - messagesNotifier: messagesNotifier, - listController: listController, - scrollController: scrollController, - ref: ref, + final messageWidget = chatIdentity.when( + skipError: true, + data: + (identity) => GestureDetector( + onLongPress: () { + if (!isSelectionMode.value) { + toggleSelectionMode(); + toggleMessageSelection(message.id); + } + }, + onTap: () { + if (isSelectionMode.value) { + toggleMessageSelection(message.id); + } + }, + child: Container( + color: + selectedMessages.value.contains(message.id) + ? Theme.of( + context, + ).colorScheme.primaryContainer.withOpacity(0.3) + : null, + child: Stack( + children: [ + MessageItem( + key: settings.disableAnimation ? key : null, + message: message, + isCurrentUser: identity?.id == message.senderId, + onAction: + isSelectionMode.value + ? null + : (action) { + switch (action) { + case MessageItemAction.delete: + messagesNotifier.deleteMessage( + message.id, ); - }, - progress: - attachmentProgress.value[message.id], - showAvatar: isLastInGroup, - isSelectionMode: isSelectionMode.value, - isSelected: selectedMessages.value - .contains(message.id), - onToggleSelection: toggleMessageSelection, - onEnterSelectionMode: () { - if (!isSelectionMode.value) { - toggleSelectionMode(); - } - }, - ), - if (selectedMessages.value.contains( - message.id, - )) - ...([ - Positioned( - top: 8, - right: 8, - child: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.primary, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check, - size: 12, - color: - Theme.of( - context, - ).colorScheme.onPrimary, - ), - ), - ), - ]), - ], - ), + case MessageItemAction.edit: + messageEditingTo.value = + message.toRemoteMessage(); + messageController.text = + messageEditingTo.value?.content ?? + ''; + attachments.value = + messageEditingTo.value!.attachments + .map( + (e) => + UniversalFile.fromAttachment( + e, + ), + ) + .toList(); + case MessageItemAction.forward: + messageForwardingTo.value = + message.toRemoteMessage(); + case MessageItemAction.reply: + messageReplyingTo.value = + message.toRemoteMessage(); + case MessageItemAction.resend: + messagesNotifier.retryMessage( + message.id, + ); + } + }, + onJump: + (messageId) => scrollToMessage( + messageId: messageId, + messageList: messageList, + messagesNotifier: messagesNotifier, + listController: listController, + scrollController: scrollController, + ref: ref, ), - ), - loading: - () => MessageItem( - message: message, - isCurrentUser: false, - onAction: null, - progress: null, - showAvatar: false, - onJump: (_) {}, - ), - error: (_, _) => const SizedBox.shrink(), - ); - - return settings.disableAnimation - ? messageWidget - : TweenAnimationBuilder( - key: key, - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration( - milliseconds: 400 + (index % 5) * 50, - ), // Staggered delay - curve: Curves.easeOutCubic, - builder: (context, animationValue, child) { - return Transform.translate( - offset: Offset( - 0, - 20 * (1 - animationValue), - ), // Slide up from bottom - child: Opacity( - opacity: animationValue, - child: child, - ), - ); - }, - child: messageWidget, - ); - }, - ), - ) - : SuperListView.builder( - listController: listController, - padding: EdgeInsets.only( - top: 16, - bottom: - MediaQuery.of(context).padding.bottom + - 8 + - inputHeight.value, - ), - controller: scrollController, - reverse: true, // Show newest messages at the bottom - itemCount: messageList.length, - findChildIndexCallback: (key) { - if (key is! ValueKey) return null; - final messageId = key.value.substring(messageKeyPrefix.length); - final index = messageList.indexWhere( - (m) => (m.nonce ?? m.id) == messageId, - ); - // Return null for invalid indices to let SuperListView handle it properly - return index >= 0 ? index : null; - }, - extentEstimation: (_, _) => 40, - itemBuilder: (context, index) { - final message = messageList[index]; - final nextMessage = - index < messageList.length - 1 - ? messageList[index + 1] - : null; - final isLastInGroup = - nextMessage == null || - nextMessage.senderId != message.senderId || - nextMessage.createdAt - .difference(message.createdAt) - .inMinutes - .abs() > - 3; - - // Use a stable animation key that doesn't change during message lifecycle - final key = Key( - '$messageKeyPrefix${message.nonce ?? message.id}', - ); - - final messageWidget = chatIdentity.when( - skipError: true, - data: - (identity) => GestureDetector( - onLongPress: () { - if (!isSelectionMode.value) { - toggleSelectionMode(); - toggleMessageSelection(message.id); - } - }, - onTap: () { - if (isSelectionMode.value) { - toggleMessageSelection(message.id); - } - }, - child: Container( - color: - selectedMessages.value.contains(message.id) - ? Theme.of(context) - .colorScheme - .primaryContainer - .withOpacity(0.3) - : null, - child: Stack( - children: [ - MessageItem( - key: settings.disableAnimation ? key : null, - message: message, - isCurrentUser: identity?.id == message.senderId, - onAction: - isSelectionMode.value - ? null - : (action) { - switch (action) { - case MessageItemAction.delete: - messagesNotifier.deleteMessage( - message.id, - ); - case MessageItemAction.edit: - messageEditingTo.value = - message.toRemoteMessage(); - messageController.text = - messageEditingTo - .value - ?.content ?? - ''; - attachments.value = - messageEditingTo - .value! - .attachments - .map( - (e) => - UniversalFile.fromAttachment( - e, - ), - ) - .toList(); - case MessageItemAction.forward: - messageForwardingTo.value = - message.toRemoteMessage(); - case MessageItemAction.reply: - messageReplyingTo.value = - message.toRemoteMessage(); - case MessageItemAction.resend: - messagesNotifier.retryMessage( - message.id, - ); - } - }, - onJump: (messageId) { - scrollToMessage( - messageId: messageId, - messageList: messageList, - messagesNotifier: messagesNotifier, - listController: listController, - scrollController: scrollController, - ref: ref, - ); - }, - progress: attachmentProgress.value[message.id], - showAvatar: isLastInGroup, - isSelectionMode: isSelectionMode.value, - isSelected: selectedMessages.value.contains( - message.id, - ), - onToggleSelection: toggleMessageSelection, - onEnterSelectionMode: () { - if (!isSelectionMode.value) { - toggleSelectionMode(); - } - }, - ), - if (selectedMessages.value.contains(message.id)) - ...([ - Positioned( - top: 8, - right: 8, - child: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.primary, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check, - size: 12, - color: - Theme.of( - context, - ).colorScheme.onPrimary, - ), - ), - ), - ]), - ], + progress: attachmentProgress.value[message.id], + showAvatar: isLastInGroup, + isSelectionMode: isSelectionMode.value, + isSelected: selectedMessages.value.contains( + message.id, ), + onToggleSelection: toggleMessageSelection, + onEnterSelectionMode: () { + if (!isSelectionMode.value) toggleSelectionMode(); + }, ), - ), - loading: - () => MessageItem( - message: message, - isCurrentUser: false, - onAction: null, - progress: null, - showAvatar: false, - onJump: (_) {}, - ), - error: (_, _) => const SizedBox.shrink(), - ); + if (selectedMessages.value.contains(message.id)) + Positioned( + top: 8, + right: 8, + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon( + Icons.check, + size: 12, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + ], + ), + ), + ), + loading: + () => MessageItem( + message: message, + isCurrentUser: false, + onAction: null, + progress: null, + showAvatar: false, + onJump: (_) {}, + ), + error: (_, _) => const SizedBox.shrink(), + ); - return settings.disableAnimation - ? messageWidget - : TweenAnimationBuilder( - key: key, - tween: Tween(begin: 0.0, end: 1.0), - duration: Duration( - milliseconds: 400 + (index % 5) * 50, - ), // Staggered delay - curve: Curves.easeOutCubic, - builder: (context, animationValue, child) { - return Transform.translate( - offset: Offset( - 0, - 20 * (1 - animationValue), - ), // Slide up from bottom - child: Opacity(opacity: animationValue, child: child), - ); - }, - child: messageWidget, - ); - }, - ); + return settings.disableAnimation + ? messageWidget + : TweenAnimationBuilder( + key: key, + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration(milliseconds: 400 + (index % 5) * 50), + curve: Curves.easeOutCubic, + builder: + (context, animationValue, child) => Transform.translate( + offset: Offset(0, 20 * (1 - animationValue)), + child: Opacity(opacity: animationValue, child: child), + ), + child: messageWidget, + ); + }, + ), + ); return AppScaffold( appBar: AppBar( diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index b8576ca1..ec72d610 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -314,9 +314,6 @@ class AppScaffold extends HookConsumerWidget { return null; }, []); - final appBarHeight = appBar?.preferredSize.height ?? 0; - final safeTop = MediaQuery.of(context).padding.top; - final noBackground = isNoBackground ?? isWideScreen(context); final builtWidget = Focus( @@ -325,16 +322,7 @@ class AppScaffold extends HookConsumerWidget { extendBody: extendBody ?? true, extendBodyBehindAppBar: true, backgroundColor: Colors.transparent, - body: Column( - children: [ - IgnorePointer( - child: SizedBox( - height: appBar != null ? appBarHeight + safeTop : 0, - ), - ), - if (body != null) Expanded(child: body!), - ], - ), + body: body, appBar: appBar, bottomNavigationBar: bottomNavigationBar, bottomSheet: bottomSheet,